第一篇:初探j(luò)ava內(nèi)存機(jī)制_堆和棧
初探j(luò)ava內(nèi)存機(jī)制_堆和棧
問(wèn)題的引入:
問(wèn)題一:
String str1 = “abc”;
String str2 = “abc”;
System.out.println(str1==str2);//true
問(wèn)題二:
String str1 =new String(“abc”);
String str2 =new String(“abc”);
System.out.println(str1==str2);// false
問(wèn)題三:
String s1 = “ja”;
String s2 = “va”;
String s3 = “java”;
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
System.out.println(s3.equals(s4));//true
由于以上問(wèn)題讓人含糊不清,于是特地搜集了一些有關(guān)java內(nèi)存分配的資料,以下是網(wǎng)摘:
Java 中的堆和棧
Java把內(nèi)存劃分成兩種:一種是棧內(nèi)存,一種是堆內(nèi)存。
在函數(shù)中定義的一些基本類(lèi)型的變量和對(duì)象的引用變量都在函數(shù)的棧內(nèi)存中分配。
當(dāng)在一段代碼塊定義一個(gè)變量時(shí),Java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過(guò)變量的作用域后,Java會(huì)自動(dòng)釋放掉為該變量所分配的內(nèi)存空間,該內(nèi)存空間可以立即被另作他用。
堆內(nèi)存用來(lái)存放由new創(chuàng)建的對(duì)象和數(shù)組。
在堆中分配的內(nèi)存,由Java虛擬機(jī)的自動(dòng)垃圾回收器來(lái)管理。
在堆中產(chǎn)生了一個(gè)數(shù)組或?qū)ο蠛螅€可以在棧中定義一個(gè)特殊的變量,讓棧中這個(gè)變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址,棧中的這個(gè)變量就成了數(shù)組或?qū)ο蟮囊米兞俊?/p>
引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€(gè)名稱(chēng),以后就可以在程序中使用棧中的引用變量來(lái)訪問(wèn)堆中的數(shù)組或?qū)ο蟆?/p>
具體的說(shuō):
棧與堆都是Java用來(lái)在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動(dòng)管理?xiàng):投眩绦騿T不能直接地設(shè)置?;蚨?。
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類(lèi)的(對(duì)象從中分配空間。這些對(duì)象通過(guò)new、newarray、anewarray和multianewarray等指令建立,它們不需要程序代碼來(lái)顯式的釋放。堆是由垃圾回收來(lái)負(fù)責(zé)的,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。
棧的優(yōu)勢(shì)是,存取速度比堆要快,僅次于寄存器,棧數(shù)據(jù)可以共享。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類(lèi)型的變量(,int, short, long, byte, float, double, boolean, char)和對(duì)象句柄。
棧有一個(gè)很重要的特殊性,就是存在棧中的數(shù)據(jù)可以共享。假設(shè)我們同時(shí)定義:
int a = 3;
int b = 3;
編譯器先處理int a = 3;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用,然后查找棧中是否有3這個(gè)值,如果沒(méi)找到,就將3存放進(jìn)來(lái),然后將a指向3。接著處理int b = 3;在創(chuàng)建完b的引用變量后,因?yàn)樵跅V幸呀?jīng)有3這個(gè)值,便將b直接指向3。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況。這時(shí),如果再令a=4;那么編譯器會(huì)重新搜索棧中是否有4值,如果沒(méi)有,則將4存放進(jìn)來(lái),并令a指向4;如果已經(jīng)有了,則直接將a指向這個(gè)地址。因此a值的改變不會(huì)影響到b的值。要注意這種數(shù)據(jù)的共享與兩個(gè)對(duì)象的引用同時(shí)指向一個(gè)對(duì)象的這種共享是不同的,因?yàn)檫@種情況a的修改并不會(huì)影響到b, 它是由編譯器完成的,它有利于節(jié)省空間。而一個(gè)對(duì)象引用變量修改了這個(gè)對(duì)象的內(nèi)部狀態(tài),會(huì)影響到另一個(gè)對(duì)象引用變量。
String是一個(gè)特殊的包裝類(lèi)數(shù)據(jù)??梢杂茫?/p>
String str = new String(“abc”);
String str = “abc”;
兩種的形式來(lái)創(chuàng)建,第一種是用new()來(lái)新建對(duì)象的,它會(huì)在存放于堆中。每調(diào)用一次就會(huì)創(chuàng)建一個(gè)新的對(duì)象。
而第二種是先在棧中創(chuàng)建一個(gè)對(duì)String類(lèi)的對(duì)象引用變量str,然后查找棧中有沒(méi)有存放“abc”,如果沒(méi)有,則將“abc”存放進(jìn)棧,并令str指向”abc”,如果已經(jīng)有”abc” 則直接令str指向“abc”。
比較類(lèi)里面的數(shù)值是否相等時(shí),用equals()方法;當(dāng)測(cè)試兩個(gè)包裝類(lèi)的引用是否指向同一個(gè)對(duì)象時(shí),用==,下面用例子說(shuō)明上面的理論。
String str1 = “abc”;
String str2 = “abc”;
System.out.println(str1==str2);//true
可以看出str1和str2是指向同一個(gè)對(duì)象的。
String str1 =new String(“abc”);
String str2 =new String(“abc”);
System.out.println(str1==str2);// false
用new的方式是生成不同的對(duì)象。每一次生成一個(gè)。
因此用第二種方式創(chuàng)建多個(gè)”abc”字符串,在內(nèi)存中其實(shí)只存在一個(gè)對(duì)象而已.這種寫(xiě)法有利與節(jié)省內(nèi)存空間.同時(shí)它可以在一定程度上提高程序的運(yùn)行速度,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來(lái)決定是否有必要?jiǎng)?chuàng)建新對(duì)象。而對(duì)于String str = new String(“abc”);的代碼,則一概在堆中創(chuàng)建新對(duì)象,而不管其字符串值是否相等,是否有必要?jiǎng)?chuàng)建新對(duì)象,從而加重了程序的負(fù)擔(dān)。
另一方面, 要注意: 我們?cè)谑褂弥T如String str = “abc”;的格式定義類(lèi)時(shí),總是想當(dāng)然地認(rèn)為,創(chuàng)建了String類(lèi)的對(duì)象str。擔(dān)心陷阱!對(duì)象可能并沒(méi)有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對(duì)象。只有通過(guò)new()方法才能保證每次都創(chuàng)建一個(gè)新的對(duì)象。由于String類(lèi)的immutable性質(zhì),當(dāng)String變量需要經(jīng)常變換其值時(shí),應(yīng)該考慮使用StringBuffer類(lèi),以提高程序效率。
java中內(nèi)存分配策略及堆和棧的比較
2.1 內(nèi)存分配策略
按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的.靜態(tài)存儲(chǔ)分配是指在編譯時(shí)就能確定每個(gè)數(shù)據(jù)目標(biāo)在運(yùn)行時(shí)刻的存儲(chǔ)空間需求,因而在編譯時(shí)就可以給他們分配固定的內(nèi)存空間.這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)(比如可變數(shù)組)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因?yàn)樗鼈兌紩?huì)導(dǎo)致編譯程序無(wú)法計(jì)算準(zhǔn)確的存儲(chǔ)空間需求.棧式存儲(chǔ)分配也可稱(chēng)為動(dòng)態(tài)存儲(chǔ)分配,是由一個(gè)類(lèi)似于堆棧的運(yùn)行棧來(lái)實(shí)現(xiàn)的.和靜態(tài)存儲(chǔ)分配相反,在棧式存儲(chǔ)方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時(shí)是完全未知的,只有到運(yùn)行的時(shí)候才能夠知道,但是規(guī)定在運(yùn)行中進(jìn)入一個(gè)程序模塊時(shí),必須知道該程序模塊所需的數(shù)據(jù)區(qū)大小才能夠?yàn)槠浞峙鋬?nèi)存.和我們?cè)跀?shù)據(jù)結(jié)構(gòu)所熟知的棧一樣,棧式存儲(chǔ)分配按照先進(jìn)后出的原則進(jìn)行分配。
靜態(tài)存儲(chǔ)分配要求在編譯時(shí)能知道所有變量的存儲(chǔ)要求,棧式存儲(chǔ)分配要求在過(guò)程的入口處必須知道所有的存儲(chǔ)要求,而堆式存儲(chǔ)分配則專(zhuān)門(mén)負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無(wú)法確定存儲(chǔ)要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長(zhǎng)度串和對(duì)象實(shí)例.堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放.2.2 堆和棧的比較
上面的定義從編譯原理的教材中總結(jié)而來(lái),除靜態(tài)存儲(chǔ)分配之外,都顯得很呆板和難以理解,下面撇開(kāi)靜態(tài)存儲(chǔ)分配,集中比較堆和棧:
從堆和棧的功能和作用來(lái)通俗的比較,堆主要用來(lái)存放對(duì)象的,棧主要是用來(lái)執(zhí)行程序的.而這種不同又主要是由于堆和棧的特點(diǎn)決定的:在編程中,例如C/C++中,所有的方法調(diào)用都是通過(guò)棧來(lái)進(jìn)行的,所有的局部變量,形式參數(shù)都是從棧中分配內(nèi)存空間的。實(shí)際上也不是什么分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會(huì)自動(dòng)指引你到放東西的位置,你所要做的只是把東西放下來(lái)就行.退出函數(shù)的時(shí)候,修改棧指針就可以把棧中的內(nèi)容銷(xiāo)毀.這樣的模式速度最快, 當(dāng)然要用來(lái)運(yùn)行程序了.需要注意的是,在分配的時(shí)候,比如為一個(gè)即將要調(diào)用的程序模塊分配數(shù)據(jù)區(qū)時(shí),應(yīng)事先知道這個(gè)數(shù)據(jù)區(qū)的大小,也就說(shuō)是雖然分配是在程序運(yùn)行時(shí)進(jìn)行的,但是分配的大小多少是確定的,不變的,而這個(gè)“大小多少”是在編譯時(shí)確定的,不是在運(yùn)行時(shí).堆是應(yīng)用程序在運(yùn)行的時(shí)候請(qǐng)求操作系統(tǒng)分配給自己內(nèi)存,由于從操作系統(tǒng)管理的內(nèi)存分配,所以在分配和銷(xiāo)毀時(shí)都要占用時(shí)間,因此用堆的效率非常低.但是堆的優(yōu)點(diǎn)在于,編譯器不必知道要從堆里分配多少存儲(chǔ)空間,也不必知道存儲(chǔ)的數(shù)據(jù)要在堆里停留多長(zhǎng)的時(shí)間,因此,用堆保存數(shù)據(jù)時(shí)會(huì)得到更大的靈活性。事實(shí)上,面向?qū)ο蟮亩鄳B(tài)性,堆內(nèi)存分配是必不可少的,因?yàn)槎鄳B(tài)變量所需的存儲(chǔ)空間只有在運(yùn)行時(shí)創(chuàng)建了對(duì)象之后才能確定.在C++中,要求創(chuàng)建一個(gè)對(duì)象時(shí),只需用 new命令編制相關(guān)的代碼即可。執(zhí)行這些代碼時(shí),會(huì)在堆里自動(dòng)進(jìn)行數(shù)據(jù)的保存.當(dāng)然,為達(dá)到這種靈活性,必然會(huì)付出一定的代價(jià):在堆里分配存儲(chǔ)空間時(shí)會(huì)花掉更長(zhǎng)的時(shí)間!這也正是導(dǎo)致我們剛才所說(shuō)的效率低的原因,看來(lái)列寧同志說(shuō)的好,人的優(yōu)點(diǎn)往往也是人的缺點(diǎn),人的缺點(diǎn)往往也是人的優(yōu)點(diǎn)(暈~).2.3 JVM中的堆和棧
JVM是基于堆棧的虛擬機(jī).JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說(shuō),對(duì)于一個(gè)Java程序來(lái)說(shuō),它的運(yùn)行就是通過(guò)對(duì)堆棧的操作來(lái)完成的。堆棧以幀為單位保存線程的狀態(tài)。JVM對(duì)堆棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作。
我們知道,某個(gè)線程正在執(zhí)行的方法稱(chēng)為此線程的當(dāng)前方法.我們可能不知道,當(dāng)前方法使用的幀稱(chēng)為當(dāng)前幀。當(dāng)線程激活一個(gè)Java方法,JVM就會(huì)在線程的 Java堆棧里新壓入一個(gè)幀。這個(gè)幀自然成為了當(dāng)前幀.在此方法執(zhí)行期間,這個(gè)幀將用來(lái)保存參數(shù),局部變量,中間計(jì)算過(guò)程和其他數(shù)據(jù).這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的.從Java的這種分配機(jī)制來(lái)看,堆棧又可以這樣理解:堆棧(Stack)是操作系統(tǒng)在建立某個(gè)進(jìn)程時(shí)或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個(gè)線程建立的存儲(chǔ)區(qū)域,該區(qū)域具有先進(jìn)后出的特性。
每一個(gè)Java應(yīng)用都唯一對(duì)應(yīng)一個(gè)JVM實(shí)例,每一個(gè)實(shí)例唯一對(duì)應(yīng)一個(gè)堆。應(yīng)用程序在運(yùn)行中所創(chuàng)建的所有類(lèi)實(shí)例或數(shù)組都放在這個(gè)堆中,并由應(yīng)用所有的線程共享.跟C/C++不同,Java中分配堆內(nèi)存是自動(dòng)初始化的。Java中所有對(duì)象的存儲(chǔ)空間都是在堆中分配的,但是這個(gè)對(duì)象的引用卻是在堆棧中分配,也就是說(shuō)在建立一個(gè)對(duì)象時(shí)從兩個(gè)地方都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對(duì)象,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對(duì)象的指針(引用)而已。
從上面的講述中大概理清了最初三個(gè)問(wèn),希望高人能再補(bǔ)充一些您覺(jué)得重要的知識(shí)點(diǎn),謝謝!
第二篇:堆和棧全面的總結(jié)
操作系統(tǒng)中的棧:
由編譯器自動(dòng)分配和自動(dòng)釋放,一個(gè)函數(shù)對(duì)應(yīng)一個(gè)棧,用于存放函數(shù)的參數(shù)值、函數(shù)調(diào)用完成后的返回值和函數(shù)體內(nèi)的局部變量等。棧占用連續(xù)的一段內(nèi)存空間,其操作和組織方式與數(shù)據(jù)結(jié)構(gòu)中的棧十分相似。棧是為了執(zhí)行線程留出的內(nèi)存空間。當(dāng)調(diào)用函數(shù)時(shí)創(chuàng)建棧,當(dāng)函數(shù)執(zhí)行完畢,棧就被回收了。
操作系統(tǒng)中的堆:
由程序員手動(dòng)進(jìn)行內(nèi)存的申請(qǐng)與釋放。由于程序員手動(dòng)申請(qǐng)及釋放的內(nèi)存塊存放在堆中,堆中有很多內(nèi)存塊,所以堆的組織方式類(lèi)似于鏈表。操作系統(tǒng)中的堆與數(shù)據(jù)結(jié)構(gòu)中的堆完全不同。我覺(jué)得通俗的理解可以是這樣的:數(shù)據(jù)結(jié)構(gòu)中的堆是“結(jié)構(gòu)堆”,有嚴(yán)謹(jǐn)?shù)倪壿嫼筒僮鞣绞?,而操作系統(tǒng)中的堆,更像是使用鏈表將“一堆雜亂的東西”聯(lián)系起來(lái)。堆是為動(dòng)態(tài)分配預(yù)留的內(nèi)存空間,其生命周期為整個(gè)應(yīng)用程序的生命周期。當(dāng)應(yīng)用程序結(jié)束以后,堆開(kāi)始被回收。
每個(gè)線程都有一個(gè)屬于自己的棧,但每一個(gè)應(yīng)用程序通常只有一個(gè)堆(一個(gè)應(yīng)用程序使用了多個(gè)堆的情況也是有的)。當(dāng)線程被創(chuàng)建的時(shí)候,設(shè)置了棧的大小。在應(yīng)用程序啟動(dòng)的時(shí)候,設(shè)置了堆的大小。棧的大小通常是固定的,但是堆可以在需要的時(shí)候進(jìn)行擴(kuò)展,如程序員向操作系統(tǒng)申請(qǐng)更多內(nèi)存的時(shí)候。
由于棧的工作方式類(lèi)似于數(shù)據(jù)結(jié)構(gòu)中的棧,堆的工作方式類(lèi)似于鏈表,所以棧顯然會(huì)比堆快得多。按照棧的存取方式,想要釋放內(nèi)存或是新增內(nèi)存,只需要相應(yīng)移動(dòng)棧頂指針即可。堆則要首先在內(nèi)存的空閑區(qū)域?qū)ふ液线m的內(nèi)存空間,然后占用,然后指向這塊空間。顯然堆比棧要復(fù)雜得多。
接下來(lái)本來(lái)是想將棧和堆分開(kāi)進(jìn)行陳述,斟酌了一下還是決定從同一方面對(duì)棧和堆進(jìn)行比較。有了比較才明顯。
1.在創(chuàng)建棧的時(shí)候棧的大小就固定了,因?yàn)闂RB續(xù)占用一段空間。根據(jù)上文所屬的堆的特性,決定了堆的大小是動(dòng)態(tài)的,其分配和釋放也是動(dòng)態(tài)的。
2.棧中的數(shù)據(jù)過(guò)多會(huì)導(dǎo)致爆棧,比如dfs寫(xiě)搓了。而假如堆也爆了的話。。那說(shuō)明內(nèi)存也爆了。
3.每個(gè)函數(shù)的棧都是各自獨(dú)立的,但是一個(gè)應(yīng)用程序的堆是被所有的棧共享。既然提到共享,那么這里就有“并行存取”的問(wèn)題了。實(shí)際上并行存取是由堆控制的,而不是被??刂频摹?/p>
4.棧的作用域僅限于函數(shù)內(nèi)部,棧在函數(shù)結(jié)束的時(shí)候會(huì)自行釋放掉空間。但是創(chuàng)建于堆上的變量必須要手動(dòng)釋放,堆中的變量不存在作用域的問(wèn)題,因?yàn)槎咽侨值摹?/p>
5.棧中存放的是函數(shù)返回值地址、函數(shù)參數(shù),函數(shù)內(nèi)的局部變量等。堆中存放的是由程序員手動(dòng)進(jìn)行申請(qǐng)的內(nèi)存塊(malloc、new等)。
6.堆和棧都按需進(jìn)行分配。棧有嚴(yán)格的容量上限,而堆的容量上限則是“不嚴(yán)格”的。堆并沒(méi)有固定的容量上限,它與當(dāng)前的剩余內(nèi)存量有關(guān)(其實(shí)還不準(zhǔn)確,操作系統(tǒng)還有虛擬內(nèi)存或其他概念,所以堆的工作方式較為抽象)。
7.通過(guò)移動(dòng)棧頂指針即可實(shí)現(xiàn)棧內(nèi)存的分配。在堆上分配內(nèi)存的做法則是從當(dāng)前空閑的內(nèi)存中找一塊滿足大小的區(qū)域,就像鏈表的工作方式一樣。
8.只要沒(méi)有超出棧容量,??梢赃M(jìn)行任意的釋放和申請(qǐng)內(nèi)存,并不會(huì)造成內(nèi)存出現(xiàn)問(wèn)題,是安全的。而堆不同,大量申請(qǐng)和釋放小內(nèi)存塊可能會(huì)造成內(nèi)存問(wèn)題,這些小的內(nèi)存塊零散的分布在內(nèi)存中,導(dǎo)致后續(xù)大塊的內(nèi)存申請(qǐng)失敗,因?yàn)殡m然空閑的內(nèi)存足夠多,但是并不連續(xù)。這種情況下的小塊內(nèi)存叫做“堆碎片”。不過(guò)這并不是什么大問(wèn)題,具體詳見(jiàn)“操作系統(tǒng)”的有關(guān)知識(shí)。
9.棧在確定了棧底地址后,其棧頂指針從棧底地址開(kāi)始,逐漸向低地址走。也就是說(shuō)棧的存儲(chǔ)空間是從高地址走向低地址的。堆則相反,堆在申請(qǐng)空間的時(shí)候通常逐漸往高地址的方向來(lái)尋找可用內(nèi)存。
純粹的文字描述顯得枯燥無(wú)味,我們來(lái)看一些代碼:
[cpp] view plaincopyprint?
#include
using namespace std;
void func()
{
int i = 5;
int j = 3;
int k = 7;
int *p = &i;
printf(“%dn”, *p);
printf(“%dn”, *(p-1));
printf(“%dn”, *(p-2));
}
int main()
{
func();
getchar();
return 0;
}
上述代碼的結(jié)果是:5 3 7
從結(jié)果中我們可以看出兩件事:
一是棧地址是連續(xù)的,我們可以通過(guò)一個(gè)指針和一個(gè)相對(duì)的大小,來(lái)“偏移”到別的變量上去。二是從中可以看出棧地址是從高到低分布的,棧底在高地址,朝低地址的方向生長(zhǎng)。所以程序中是p-1而不是p+1。
[cpp] view plaincopyprint?
void func()
{
int *p = NULL;
// 上行代碼是個(gè)重點(diǎn)。這個(gè)指針待會(huì)會(huì)用于申請(qǐng)新的內(nèi)存。
// 此時(shí)除了它自身作為一個(gè)變量需要占用4字節(jié)的空間(指針都占4字節(jié)),沒(méi)有任何其他空間被申請(qǐng)。
// 這個(gè)指針變量是函數(shù)的局部變量,所以它被創(chuàng)建在棧上。
int num = 100;// 這個(gè)變量同樣創(chuàng)建于棧上。
int buffer[100];// 同樣的,buffer占用了棧的400字節(jié)的空間
p = new int[100];// 注意,程序員手動(dòng)申請(qǐng)了一塊空間,這400字節(jié)的內(nèi)存創(chuàng)建于堆上。
// 所以此刻p的狀態(tài)是:p為函數(shù)局部變量,它指向了一塊全局范圍的內(nèi)存空間。}
// 函數(shù)體結(jié)束。上述函數(shù)有個(gè)嚴(yán)重的問(wèn)題,那就是指針p的內(nèi)存泄露。
// 正確的做法是在函數(shù)最后delete掉這塊內(nèi)存,或是返回這塊內(nèi)存的地址以供繼續(xù)使用。
接下來(lái)我們來(lái)了解一下當(dāng)調(diào)用一個(gè)函數(shù)的時(shí)候所發(fā)生的事情:
首先操作系統(tǒng)為這個(gè)函數(shù)分配了一個(gè)棧,因?yàn)樵谡{(diào)用完這個(gè)函數(shù)以后需要能正確返回到下一條語(yǔ)句并繼續(xù)執(zhí)行,所以第一步是將調(diào)用完函數(shù)的下一條指令的地址壓入棧。這樣當(dāng)函數(shù)調(diào)用完成,棧頂指針一點(diǎn)點(diǎn)釋放內(nèi)存以后,棧頂指針指向了這個(gè)地址,就能返回到正確的位置繼續(xù)執(zhí)行了。
[cpp] view plaincopyprint?
int main()
{
func();
printf(“%dn”, 100);
return 0;
}
比如上述代碼,在調(diào)用func之前,首先把func的下一條語(yǔ)句,也就是printf語(yǔ)句的地址,存在棧中。這樣函數(shù)調(diào)用完成后就能正確返回到這個(gè)printf并繼續(xù)往后執(zhí)行了。注意這里的地址是指令地址,而不是變量地址什么的。它有那么點(diǎn)類(lèi)似于操作系統(tǒng)中的程序計(jì)數(shù)器(PC,即Program Counter)。然后把實(shí)參從右到左的順序依次入棧(大多數(shù)的C/C++編譯器為從右到左)接著是函數(shù)中的各種局部變量。要注意的是函數(shù)中的static變量是不入棧的。全局變量和static變量在編譯的時(shí)候就已經(jīng)在靜態(tài)存儲(chǔ)區(qū)分配好內(nèi)存了。
如果這個(gè)時(shí)候該函數(shù)又調(diào)用了其它函數(shù),過(guò)程也是一樣的,首先是返回地址,然后是參數(shù)和局部變量。這樣在每層調(diào)用結(jié)束,棧頂指針不斷下降(釋放內(nèi)存)的時(shí)候,就能正確返回到之前調(diào)用的位置并繼續(xù)往下執(zhí)行了。
出棧,或者說(shuō)釋放內(nèi)存的過(guò)程,根據(jù)棧的特性,是相反的,所以就不贅述了。
一個(gè) C或C++程序,它眼中的內(nèi)存地址分分為這么五個(gè)區(qū)域:
棧區(qū)(stack)、堆區(qū)(heap)、全局靜態(tài)區(qū)(static)、文字常量區(qū)和程序指令區(qū)。
棧區(qū)和堆區(qū)前面已經(jīng)介紹過(guò),全局靜態(tài)區(qū)用于存放全局變量和靜態(tài)static靜態(tài)變量,全局靜態(tài)區(qū)分為兩塊內(nèi)容:一塊用于初始化以后的全局變量和靜態(tài)變量,一塊用于未初始化的全局變量和靜態(tài)變量。全局靜態(tài)區(qū)和堆一樣,程序結(jié)束后由操作系統(tǒng)進(jìn)行釋放。文字常量區(qū)用于存放常量字符串,程序結(jié)束后由操作系統(tǒng)進(jìn)行釋放。程序指令區(qū)最好理解,就是存放程序代碼的二進(jìn)制指令。
[cpp] view plaincopyprint?
int cnt;// 存放在全局靜態(tài)區(qū)的未初始化區(qū)
int num = 0;// 存放在全局靜態(tài)區(qū)的已初始化區(qū)
int *p;// 存放在全局靜態(tài)區(qū)的未初始化區(qū)
int main()
{
int i, j, k;// 存放在棧區(qū)
int *pBuffer =(int *)malloc(sizeof(int)* 10);// 指針pBuffer在棧中,該內(nèi)存在堆中char *s = “hactrox”;// 指針s存放在棧中,字符串存放在文字常量區(qū)中char str[] = “hactrox”;// str和字符串存放在棧中
static int a = 0;// a存放在全局靜態(tài)區(qū)的已初始化區(qū)
}
char *s = “hactrox”;// “hactrox”在文字常量區(qū),s指向這個(gè)區(qū)域中的“hactrox”,所以這可以理解為,首先在文字常量區(qū)創(chuàng)建了這個(gè)字符串,然后s指向這個(gè)字符串這樣兩個(gè)步驟。s本身作為一個(gè)局部變量存儲(chǔ)在棧中。
// 下面的代碼是錯(cuò)誤的,指針還沒(méi)指向就直接賦值了?
int *p = 5;
// 下面的代碼才是正確的,首先要?jiǎng)?chuàng)建這個(gè)int型變量,然后p指向這個(gè)變量。new來(lái)的int變量在堆中。
int *p = new int(5);
接下來(lái)我們看一看一個(gè)非常常見(jiàn)的問(wèn)題:下述代碼有沒(méi)有什么問(wèn)題?有問(wèn)題的話問(wèn)題在哪里?
[cpp] view plaincopyprint?
#include
using namespace std;
char* f1()
{
char *s = “hactrox”;
return s;
}
char* f2()
{
char s[] = “hactrox”;
return s;
}
int main()
{
printf(“%sn”, f1());
printf(“%sn”, f2());
getchar();
return 0;
}
問(wèn)題在于第二個(gè)函數(shù),f2并不能正確返回那個(gè)字符串。在函數(shù)f1中,“hactrox”字符串創(chuàng)建于文字常量區(qū),然后返回該常量字符串的地址,因?yàn)槲淖殖A繀^(qū)的字符串是全局的,雖然指針s是局部變量,但是s在消亡前已經(jīng)把目標(biāo)地址送出來(lái)了,所以s消亡與否不是重點(diǎn),重點(diǎn)是返回的地址所指向的區(qū)域還在,所以能正確顯示。在函數(shù)f2中,“hactrox”與s均為局部變量,它們保存在棧中。雖然s同樣返回了一個(gè)地址,但這個(gè)地址所指向的內(nèi)存已經(jīng)被釋放掉了。地址有效,但目標(biāo)已無(wú)效。所以輸出的只是亂碼。
[cpp] view plaincopyprint?
#include
using namespace std;
void func()
{
char *str1 = “123”;
printf(“%xn”, str1);
char *str2 = “123”;
// 同在文字常量區(qū),編譯器可能會(huì)將str2直接指向str1所指向的內(nèi)存,// 而不是開(kāi)辟新的空間來(lái)存放第二個(gè)相同字符串。
// 通過(guò)打印str2的指針可驗(yàn)證
printf(“%xn”, str2);
char *s1 = “hactrox”;
printf(“%xn”, s1);
char *s2 = “hactrox”;
printf(“%xn”, s2);
}
int main()
{
func();
getchar();
return 0;
}
char s[] = “hactrox”;
char *s = “hactrox again”;
第二段代碼,即文字常量區(qū)變量在編譯的時(shí)候就已經(jīng)確定了,而第一段代碼,是在運(yùn)行的時(shí)候進(jìn)行賦值的。
這樣看起來(lái)貌似第二段代碼的效率要高,其實(shí)不然,當(dāng)在運(yùn)行時(shí)刻用到這兩個(gè)變量的時(shí)候,對(duì)于第一段代碼,直接讀取字符串,而對(duì)于第二段代碼,首先讀取該字符串指針,然后根據(jù)指針再讀取字符串,顯然效率就下降了。其實(shí)我覺(jué)得關(guān)注棧和堆,其實(shí)主要是關(guān)注作用域、生命周期和有效性的問(wèn)題。
指針被釋放了,不代表指針指向的內(nèi)存會(huì)被釋放。同樣的,指針指向的內(nèi)存被釋放了,不代表指針會(huì)被同步釋放或自動(dòng)指向NULL,指針依舊指向那塊已經(jīng)失效了的地址。這塊地址不能用,誰(shuí)都不能保證一塊已經(jīng)失效的地址接下來(lái)會(huì)發(fā)生什么。
第三篇:Java的內(nèi)存泄漏 總結(jié)分析
Java的內(nèi)存泄漏
歐陽(yáng)辰(yeekee@sina.com), 周欣(mailto:zhouxin@sei.pku.edu.cn), 簡(jiǎn)介: Java的一個(gè)重要優(yōu)點(diǎn)就是通過(guò)垃圾收集器(Garbage Collection,GC)自動(dòng)管理內(nèi)存的回收,程序員不需要通過(guò)調(diào)用函數(shù)來(lái)釋放內(nèi)存。因此,很多程序員認(rèn)為Java不存在內(nèi)存泄漏問(wèn)題,或者認(rèn)為即使有內(nèi)存泄漏也不是程序的責(zé)任,而是GC或JVM的問(wèn)題。其實(shí),這種想法是不正確的,因?yàn)镴ava也存在內(nèi)存泄露,但它的表現(xiàn)與C++不同。本文的標(biāo)簽: j2se, java, java內(nèi)存泄露, 內(nèi)存, 內(nèi)存泄漏, 內(nèi)存泄露 標(biāo)記本文!
發(fā)布日期: 2002 年 10 月 21 日 級(jí)別: 初級(jí)
訪問(wèn)情況: 18862 次瀏覽
評(píng)論: 4(查看 | 添加評(píng)論-登錄)平均分(59個(gè)評(píng)分)為本文評(píng)分
問(wèn)題的提出
Java的一個(gè)重要優(yōu)點(diǎn)就是通過(guò)垃圾收集器(Garbage Collection,GC)自動(dòng)管理內(nèi)存的回收,程序員不需要通過(guò)調(diào)用函數(shù)來(lái)釋放內(nèi)存。因此,很多程序員認(rèn)為Java不存在內(nèi)存泄漏問(wèn)題,或者認(rèn)為即使有內(nèi)存泄漏也不是程序的責(zé)任,而是GC或JVM的問(wèn)題。其實(shí),這種想法是不正確的,因?yàn)镴ava也存在內(nèi)存泄露,但它的表現(xiàn)與C++不同。
隨著越來(lái)越多的服務(wù)器程序采用Java技術(shù),例如JSP,Servlet,EJB等,服務(wù)器程序往往長(zhǎng)期運(yùn)行。另外,在很多嵌入式系統(tǒng)中,內(nèi)存的總量非常有限。內(nèi)存泄露問(wèn)題也就變得十分關(guān)鍵,即使每次運(yùn)行少量泄漏,長(zhǎng)期運(yùn)行之后,系統(tǒng)也是面臨崩潰的危險(xiǎn)。
回頁(yè)首
Java是如何管理內(nèi)存
為了判斷Java中是否有內(nèi)存泄露,我們首先必須了解Java是如何管理內(nèi)存的。Java的內(nèi)存管理就是對(duì)象的分配和釋放問(wèn)題。在Java中,程序員需要通過(guò)關(guān)鍵字new為每個(gè)對(duì)象申請(qǐng)內(nèi)存空間(基本類(lèi)型除外),所有的對(duì)象都在堆(Heap)中分配空間。另外,對(duì)象的釋放是由GC決定和執(zhí)行的。在Java中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是有GC完成的,這種收支兩條線的方法確實(shí)簡(jiǎn)化了程序員的工作。但同時(shí),它也加重了JVM的工作。這也是Java程序運(yùn)行速度較慢的原因之一。因?yàn)?,GC為了能夠正確釋放對(duì)象,GC必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài),包括對(duì)象的申請(qǐng)、引用、被引用、賦值等,GC都需要進(jìn)行監(jiān)控。
監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地、及時(shí)地釋放對(duì)象,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用。
為了更好理解GC的工作原理,我們可以將對(duì)象考慮為有向圖的頂點(diǎn),將引用關(guān)系考慮為圖的有向邊,有向邊從引用者指向被引對(duì)象。另外,每個(gè)線程對(duì)象可以作為一個(gè)圖的起始頂點(diǎn),例如大多程序從main進(jìn)程開(kāi)始執(zhí)行,那么該圖就是以main進(jìn)程頂點(diǎn)開(kāi)始的一棵根樹(shù)。在這個(gè)有向圖中,根頂點(diǎn)可達(dá)的對(duì)象都是有效對(duì)象,GC將不回收這些對(duì)象。如果某個(gè)對(duì)象(連通子圖)與這個(gè)根頂點(diǎn)不可達(dá)(注意,該圖為有向圖),那么我們認(rèn)為這個(gè)(這些)對(duì)象不再被引用,可以被GC回收。
以下,我們舉一個(gè)例子說(shuō)明如何用有向圖表示內(nèi)存管理。對(duì)于程序的每一個(gè)時(shí)刻,我們都有一個(gè)有向圖表示JVM的內(nèi)存分配情況。以下右圖,就是左邊程序運(yùn)行到第6行的示意圖。
Java使用有向圖的方式進(jìn)行內(nèi)存管理,可以消除引用循環(huán)的問(wèn)題,例如有三個(gè)對(duì)象,相互引用,只要它們和根進(jìn)程不可達(dá)的,那么GC也是可以回收它們的。這種方式的優(yōu)點(diǎn)是管理內(nèi)存的精度很高,但是效率較低。另外一種常用的內(nèi)存管理技術(shù)是使用計(jì)數(shù)器,例如COM模型采用計(jì)數(shù)器方式管理構(gòu)件,它與有向圖相比,精度行低(很難處理循環(huán)引用的問(wèn)題),但執(zhí)行效率很高。
回頁(yè)首
什么是Java中的內(nèi)存泄露
下面,我們就可以描述什么是內(nèi)存泄漏。在Java中,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn),首先,這些對(duì)象是可達(dá)的,即在有向圖中,存在通路可以與其相連;其次,這些對(duì)象是無(wú)用的,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏,這些對(duì)象不會(huì)被GC所回收,然而它卻占用內(nèi)存。
在C++中,內(nèi)存泄漏的范圍更大一些。有些對(duì)象被分配了內(nèi)存空間,然后卻不可達(dá),由于C++中沒(méi)有GC,這些內(nèi)存將永遠(yuǎn)收不回來(lái)。在Java中,這些不可達(dá)的對(duì)象都由GC負(fù)責(zé)回收,因此程序員不需要考慮這部分的內(nèi)存泄露。
通過(guò)分析,我們得知,對(duì)于C++,程序員需要自己管理邊和頂點(diǎn),而對(duì)于Java程序員只需要管理邊就可以了(不需要管理頂點(diǎn)的釋放)。通過(guò)這種方式,Java提高了編程的效率。
因此,通過(guò)以上分析,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些。因?yàn)镴ava從語(yǔ)言上保證,任何對(duì)象都是可達(dá)的,所有的不可達(dá)對(duì)象都由GC管理。
對(duì)于程序員來(lái)說(shuō),GC基本是透明的,不可見(jiàn)的。雖然,我們只有幾個(gè)函數(shù)可以訪問(wèn)GC,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語(yǔ)言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行。因?yàn)?,不同的JVM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級(jí)別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開(kāi)始工作,也有定時(shí)執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來(lái)說(shuō),我們不需要關(guān)心這些。除非在一些特定的場(chǎng)合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對(duì)于基于Web的實(shí)時(shí)系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶(hù)不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過(guò)平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpot JVM就支持這一特性。下面給出了一個(gè)簡(jiǎn)單的內(nèi)存泄露的例子。在這個(gè)例子中,我們循環(huán)申請(qǐng)Object對(duì)象,并將所申請(qǐng)的對(duì)象放入一個(gè)Vector中,如果我們僅僅釋放引用本身,那么Vector仍然引用該對(duì)象,所以這個(gè)對(duì)象對(duì)GC來(lái)說(shuō)是不可回收的。因此,如果對(duì)象加入到Vector后,還必須從Vector中刪除,最簡(jiǎn)單的方法就是將Vector對(duì)象設(shè)置為null。
Vector v=new Vector(10);for(inti=1;i<100;i++){
} Object o=new Object();v.add(o);o=null;
//此時(shí),所有的Object對(duì)象都沒(méi)有被釋放,因?yàn)樽兞縱引用這些對(duì)象。
回頁(yè)首
如何檢測(cè)內(nèi)存泄漏
最后一個(gè)重要的問(wèn)題,就是如何檢測(cè)Java的內(nèi)存泄漏。目前,我們通常使用一些工具來(lái)檢查Java程序的內(nèi)存泄漏問(wèn)題。市場(chǎng)上已有幾種專(zhuān)業(yè)檢查Java內(nèi)存泄漏的工具,它們的基本工作原理大同小異,都是通過(guò)監(jiān)測(cè)Java程序運(yùn)行時(shí),所有對(duì)象的申請(qǐng)、釋放等動(dòng)作,將內(nèi)存管理的所有信息進(jìn)行統(tǒng)計(jì)、分析、可視化。開(kāi)發(fā)人員將根據(jù)這些信息判斷程序是否有內(nèi)存泄漏問(wèn)題。這些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。
下面,我們將簡(jiǎn)單介紹Optimizeit的基本功能和工作原理。
Optimizeit Profiler版本4.11支持Application,Applet,Servlet和Romote Application四類(lèi)應(yīng)用,并且可以支持大多數(shù)類(lèi)型的JVM,包括SUN JDK系列,IBM的JDK系列,和Jbuilder的JVM等。并且,該軟件是由Java編寫(xiě),因此它支持多種操作系統(tǒng)。Optimizeit系列還包括Thread Debugger和Code Coverage兩個(gè)工具,分別用于監(jiān)測(cè)運(yùn)行時(shí)的線程狀態(tài)和代碼覆蓋面。
當(dāng)設(shè)置好所有的參數(shù)了,我們就可以在OptimizeIt環(huán)境下運(yùn)行被測(cè)程序,在程序運(yùn)行過(guò)程中,Optimizeit可以監(jiān)視內(nèi)存的使用曲線(如下圖),包括JVM申請(qǐng)的堆(heap)的大小,和實(shí)際使用的內(nèi)存大小。另外,在運(yùn)行過(guò)程中,我們可以隨時(shí)暫停程序的運(yùn)行,甚至強(qiáng)行調(diào)用GC,讓GC進(jìn)行內(nèi)存回收。通過(guò)內(nèi)存使用曲線,我們可以整體了解程序使用內(nèi)存的情況。這種監(jiān)測(cè)對(duì)于長(zhǎng)期運(yùn)行的應(yīng)用程序非常有必要,也很容易發(fā)現(xiàn)內(nèi)存泄露。
在運(yùn)行過(guò)程中,我們還可以從不同視角觀查內(nèi)存的使用情況,Optimizeit提供了四種方式:
? 堆視角。這是一個(gè)全面的視角,我們可以了解堆中的所有的對(duì)象信息(數(shù)量和種類(lèi)),并進(jìn)行統(tǒng)計(jì)、排序,過(guò)濾。了解相關(guān)對(duì)象的變化情況。
? 方法視角。通過(guò)方法視角,我們可以得知每一種類(lèi)的對(duì)象,都分配在哪些方法中,以及它們的數(shù)量。
? 對(duì)象視角。給定一個(gè)對(duì)象,通過(guò)對(duì)象視角,我們可以顯示它的所有出引用和入引用對(duì)象,我們可以了解這個(gè)對(duì)象的所有引用關(guān)系。
? 引用圖。給定一個(gè)根,通過(guò)引用圖,我們可以顯示從該頂點(diǎn)出發(fā)的所有出引用。在運(yùn)行過(guò)程中,我們可以隨時(shí)觀察內(nèi)存的使用情況,通過(guò)這種方式,我們可以很快找到那些長(zhǎng)期不被釋放,并且不再使用的對(duì)象。我們通過(guò)檢查這些對(duì)象的生存周期,確認(rèn)其是否為內(nèi)存泄露。在實(shí)踐當(dāng)中,尋找內(nèi)存泄露是一件非常麻煩的事情,它需要程序員對(duì)整個(gè)程序的代碼比較清楚,并且需要豐富的調(diào)試經(jīng)驗(yàn),但是這個(gè)過(guò)程對(duì)于很多關(guān)鍵的Java程序都是十分重要的。
綜上所述,Java也存在內(nèi)存泄露問(wèn)題,其原因主要是一些對(duì)象雖然不再被使用,但它們?nèi)匀槐灰谩榱私鉀Q這些問(wèn)題,我們可以通過(guò)軟件工具來(lái)檢查內(nèi)存泄露,檢查的主要原理就是暴露出所有堆中的對(duì)象,讓程序員尋找那些無(wú)用但仍被引用的對(duì)象。