Java與C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”爆阶,墻外面的人想進(jìn)去佩研,墻里面的人卻想出來捣域。
Java虛擬機(jī)在執(zhí)行Java程序的過程中會把它管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域挂谍。
一掏觉、程序計數(shù)器
程序計數(shù)器可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在JVM的概念模型里榴鼎,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令伯诬。
由于JVM的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,為了在線程切換后能恢復(fù)到正確的執(zhí)行位置巫财,每條線程都需要有一個獨(dú)立的程序計數(shù)器盗似,獨(dú)立存儲,互不影響平项。所以赫舒,程序計數(shù)器是線程私有的內(nèi)存區(qū)域。
如果線程執(zhí)行的是一個Java方法闽瓢,計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址接癌;
如果線程執(zhí)行的是一個Native方法,計數(shù)器的值為空扣讼。Java虛擬機(jī)規(guī)范中唯一一個沒有規(guī)定任何OutOfMemoryError情況的區(qū)域缺猛。
二、Java虛擬機(jī)棧
-
Java虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法執(zhí)行的同時會創(chuàng)建一個棧幀,棧幀用于存儲局部變量表荔燎、操作數(shù)棧耻姥、動態(tài)鏈接、方法出口等信息有咨。每個方法從調(diào)用直至執(zhí)行完成的過程琐簇,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。
Java虛擬機(jī)棧是線程私有的座享,它的生命周期與線程相同婉商。
程序員主要關(guān)注的stack棧內(nèi)存,就是虛擬機(jī)棧中局部變量表部分征讲。
局部變量表存放了編譯時期可知的各種基本數(shù)據(jù)類型和對象引用据某。
局部變量表所需的內(nèi)存空間在編譯時期完成分配,當(dāng)進(jìn)入一個方法時诗箍,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會改變局部變量表的大小挽唉。Java虛擬機(jī)規(guī)范對這個區(qū)域規(guī)定了兩種異常情況:
- 如果線程請求的棧深度大于虛擬機(jī)所允許的深度滤祖,將拋出
StackOverflowError
異常; - 如果虛擬機(jī)椘孔眩可以動態(tài)擴(kuò)展匠童,如果擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出
OutOfMemoryError
異常塑顺;
(當(dāng)前大部分JVM都可以動態(tài)擴(kuò)展汤求,只不過JVM規(guī)范也允許固定長度的虛擬機(jī)棧)
三、本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的严拒,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)(也就是字節(jié)碼)扬绪,而本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。
Java虛擬機(jī)規(guī)范對本地方法棧使用的語言裤唠、使用方法與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定挤牛,因此可以由虛擬機(jī)自由實現(xiàn)。例如:HotSpot虛擬機(jī)直接將本地方法棧和虛擬機(jī)棧合二為一种蘸。
同虛擬機(jī)棧相同墓赴,Java虛擬機(jī)規(guī)范對這個區(qū)域也規(guī)定了兩種異常情況
StackOverflowError
和OutOfMemoryError
異常。
四航瞭、Java堆
Java堆是被所有的線程共享的一塊內(nèi)存區(qū)域诫硕,在虛擬機(jī)啟動時創(chuàng)建。
Java堆的唯一目的就是存放對象實例刊侯,幾乎所有的對象實例都在這里分配內(nèi)存章办。Java堆是垃圾回收器管理的主要區(qū)域,因此也被稱為"GC堆"。
從內(nèi)存回收的角度看纲菌,由于現(xiàn)在收集器基本都采用分代收集算法挠日,所以Java堆可以細(xì)分為:新生代、老生代翰舌;
從內(nèi)存分配的角度看嚣潜,線程共享的Java堆可能劃分出多個線程私有的分配緩沖區(qū)(TLAB);
不論如何劃分椅贱,都與存放的內(nèi)容無關(guān)懂算,無論哪個區(qū)域,存儲的仍然是對象實例庇麦。Java虛擬機(jī)規(guī)范規(guī)定计技,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可山橄,就像我們的磁盤空間一樣垮媒。在實現(xiàn)上,既可以是固定大小的航棱,也可以是可擴(kuò)展的睡雇,不過當(dāng)前主流JVM都是按照可擴(kuò)展來實現(xiàn)的。
Java虛擬機(jī)規(guī)范規(guī)定饮醇,如果在堆上沒有內(nèi)存完成實例分配它抱,并且堆上也無法再擴(kuò)展時,將會拋出
OutOfMemoryError
異常朴艰。內(nèi)存泄露和內(nèi)存溢出
Java堆內(nèi)存的OOM異常是非常常見的異常情況观蓄,重點(diǎn)是根據(jù)內(nèi)存中的對象是否是必要的,來弄清楚到底是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow).
- 內(nèi)存泄露:指程序中一些對象不會被GC所回收祠墅,它始終占用內(nèi)存侮穿,即被分配的對象引用鏈可達(dá)但已無用。(可用內(nèi)存減少)
- 內(nèi)存溢出:程序運(yùn)行過程中無法申請到足夠的內(nèi)存而導(dǎo)致的一種錯誤饵隙。內(nèi)存溢出通常發(fā)生于OLD段或Perm段垃圾回收后撮珠,仍然無內(nèi)存空間容納新的Java對象的情況。
內(nèi)存泄露是內(nèi)存溢出的一種誘因金矛,不是唯一因素芯急。
五、方法區(qū)
方法區(qū)也是被所有的線程共享的一塊內(nèi)存區(qū)域驶俊。它用于存儲已被虛擬機(jī)加載的類信息娶耍、常量、靜態(tài)變量饼酿、即時編譯器編譯后的代碼等數(shù)據(jù)榕酒。
Java虛擬機(jī)規(guī)范對方法區(qū)的限制非常寬松胚膊,除了和Java堆一樣 不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展之外,還可以選擇不實現(xiàn)垃圾回收想鹰。
這區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和類型的卸載紊婉,一般而言,這個區(qū)域的內(nèi)存回收比較難以令人滿意辑舷,尤其是類型的回收喻犁,條件相當(dāng)苛刻,但是這部分區(qū)域的內(nèi)存回收確實是必要的何缓。Java虛擬機(jī)規(guī)范規(guī)定肢础,當(dāng)方法區(qū)無法滿足內(nèi)存分配的需求時,將拋出
OutOfMemoryError
異常碌廓。運(yùn)行時常量池
運(yùn)行時常量池是方法區(qū)的一部分传轰。CLass文件中除了有類的版本、字段谷婆、方法慨蛙、接口等描述信息外,還有一項信息是常量池波材,用于存放編譯期生成的各種字面量和符號引用股淡,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放。
運(yùn)行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態(tài)性廷区,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置入CLass文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池贾铝,運(yùn)行期間也可能將新的常量放入池中隙轻,這種特性被開發(fā)人員利用比較多的就是String類的intern()方法。String.intern()
String.intern()
是一個Native方法垢揩,它的作用是:如果字符串常量池中已經(jīng)包含了一個等于此String對象的字符串玖绿,則返回代表池中這個字符串的String對象;否則叁巨,將此String對象包含的字符串添加到常量池中斑匪,并且返回此字符串的引用。
public static void main(String[] args) {
String str1 = new StringBuilder("計算機(jī)").append("軟件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
這段代碼在JDK1.6中運(yùn)行锋勺,會得到兩個false蚀瘸,而在JDK1.7中運(yùn)行,會得到一個true和一個false庶橱。原因是:
- 在JDK1.6中
intern()
方法會把首次遇到的字符串實例復(fù)制到永久代中贮勃,返回的也是永久代中這個字符串實例的引用,而由StringBuilder創(chuàng)建的字符串實例在Java堆上苏章,所以必然不是一個引用寂嘉。 - 在JDK1.7中
intern()
方法不會復(fù)制實例奏瞬,只是在常量池中記錄首次出現(xiàn)的實例引用,因此intern()
返回的引用和由StringBuilder創(chuàng)建的字符串實例是同一個泉孩。 - str2返回false是因為Java這個字符串在執(zhí)行
StringBuilder("ja").append("va").toString()
之前已經(jīng)出現(xiàn)過硼端,字符串常量池中已經(jīng)有它的引用了,不符合首次出現(xiàn)的原則寓搬,而"計算機(jī)軟件"
這個字符串是首次出現(xiàn)的珍昨。
推薦閱讀:《深入理解Java虛擬機(jī)》周志明著
[2015.08.29]