運行時數(shù)據(jù)區(qū)域
Java虛擬機在執(zhí)行java程序的過程中會將它所管理的內(nèi)存劃分為若干不同的數(shù)據(jù)區(qū)域,這些區(qū)域有各自用途,以及創(chuàng)建和銷毀時間,有的區(qū)域隨著虛擬機進(jìn)程的啟動而存在,有些區(qū)域依賴用戶線程的啟動和結(jié)束而建立和銷毀。
Java虛擬機運行時數(shù)據(jù)區(qū)如下圖所示:

從圖中,我們看到了5大區(qū)域:線程共享的方法區(qū)和堆,線程私有的java虛擬機棧,本地方法棧以及程序計數(shù)器。
程序計數(shù)器:(Program Counter Register)這個區(qū)域是唯一一個不會拋出OutOfMemoryError異常的區(qū)域。它是一塊比較小的內(nèi)存,是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計算器,各條線程之間計算器互不影響。
Java虛擬機棧:也是線程私有的,它的生命周期與線程相同,它描述的是java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的時候會創(chuàng)建一個棧幀用來存儲局部變量,操作數(shù),動態(tài)鏈接等。每個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧和出棧的過程。
本地方法棧:和java虛擬機棧功能類似,只不過java虛擬機棧執(zhí)行的是java字節(jié)碼而本地方法棧執(zhí)行的是Native方法。
Java堆:Java堆是虛擬機所管理的內(nèi)存中最大的一塊,Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,幾乎所有的對象實例都在這里分配內(nèi)存。java堆可以細(xì)分出新生代和老年代,再細(xì)致一點可以分為:Eden,F(xiàn)rom Survivor,To Survivor等空間。
方法區(qū):該區(qū)域用來存儲已經(jīng)被虛擬機加載過來的類信息,常量,靜態(tài)變量等。方法區(qū)導(dǎo)致內(nèi)存問題實例請參考:Android性能優(yōu)化-方法區(qū)導(dǎo)致內(nèi)存問題實例分析。
以上講完了JVM運行時內(nèi)存區(qū)域的5大塊,同時需要補充的一點是還有一個運行時常量池,它也是方法區(qū)的一部分。Class文件中除了有類的版本,字段,接口,方法等描述信息外,還有一項信息就是常量池,用來存放編譯時期生成的各種字面量和符號引用。但是需要注意的是:Java語言并不要求常量一定是在編譯期間產(chǎn)生,也就是并非與裝入class文件中的常量池的內(nèi)容才能進(jìn)入到方法區(qū)運行時常量池,運行期間也可能將新的常量放入常量池中,如String.intern()方法。
運行時不同數(shù)據(jù)區(qū)域異常
(1)除程序計算器外,虛擬機內(nèi)存的其他幾個運行時區(qū)都會發(fā)生OutOfMemoryError。
(2)使Java堆發(fā)生內(nèi)存溢出的思路:只要不斷創(chuàng)建對象,并保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機制清除這些對象即可。
(3)使方法區(qū)發(fā)生類導(dǎo)致的內(nèi)存溢出基本思路:在運行時產(chǎn)生大量的類去填滿方法區(qū),也就是在運行時動態(tài)產(chǎn)生很多的類,直到方法區(qū)內(nèi)存溢出。所以頻繁動態(tài)產(chǎn)生很多類時,需要注意方法區(qū)內(nèi)存溢出,具體內(nèi)容請參考Android性能優(yōu)化-方法區(qū)導(dǎo)致內(nèi)存問題實例分析。
(4)虛擬機棧和本地方法棧兩種異常:
a) 如果線程請求的棧深度大于虛擬機所允許的最大深度(即方法調(diào)用深度超過最大允許深度),將拋出StackOverFlowError異常.
b) 如果虛擬機在擴展棧時無法申請到足夠的內(nèi)存,則拋出utOfMemoryError異常。
HotSpot虛擬機對象創(chuàng)建、對象內(nèi)存布局、對象訪問定位
對象創(chuàng)建
(1)對象創(chuàng)建的幾種方法:new、克隆、反序列化;
(2)對象創(chuàng)建過程:
步驟一:虛擬機在遇到new指令時,首先會去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用并且檢查該符號引用代表的類是否已經(jīng)被加載,解析和初始化過。如果沒有,那就必須先執(zhí)行相應(yīng)的類加載。
步驟二:在類經(jīng)過加載檢查后,虛擬機就需要為新生對象分配內(nèi)存了。對象所需要的內(nèi)存大小在類加載完成之后就可以確定。對象分配內(nèi)存空間的任務(wù)等同于把一塊確定大小的內(nèi)存從java堆中劃分出來,如果內(nèi)存絕對規(guī)整,采用指針碰撞分配方式(即移動指針到與對象大小相等的距離),如果內(nèi)存不規(guī)整采用空間列表的分配方式。
步驟三:在分配完內(nèi)存之后,虛擬機需要將分配到的內(nèi)存空間初始化為零值。
步驟四:接下來,虛擬機會對對象進(jìn)行必要的設(shè)置,如對象的hash碼,對象的GC分代信息等。
步驟五:最后執(zhí)行對象的init方法把對象按照程序員意愿進(jìn)行初始化,這樣一個真正的對象才算完全產(chǎn)生出來。
對象內(nèi)存布局
對象內(nèi)存布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)、對齊填充。
對象頭:主要存儲了2部分信息,第一部分是對象自身運行的數(shù)據(jù),如hashcode,GC分代等信息;
第二部分是類型指針,就是對象對它的類元數(shù)據(jù)指針,其實就是一個引用。虛擬機通過這個指針(引用)來確定對象是哪個類的實例。
實例數(shù)據(jù)(Instance Data):對象真正存儲的有效信息,也就是程序代碼中所寫的各種類型的字段內(nèi)容。
對齊填充(Padding):這個不是必然存在的。HotSpot虛擬機自動內(nèi)存管理要求對象起始地址必須是8字節(jié)的整數(shù)倍,也就是對象大小必須是8字節(jié)的整數(shù)倍,對象實例數(shù)據(jù)部分沒有對齊時,需要通過對齊填充來補齊。
對象的訪問定位
我們知道了對象的創(chuàng)建,內(nèi)存布局等相關(guān)內(nèi)容之后,需要知道存儲的對象如何找到呢?這就涉及到對象的定位問題了。我們java程序需要通過棧上的引用數(shù)據(jù)來操作具體的對象。對對象的訪問方式取決于虛擬機的實現(xiàn),目前比較主流的有句柄和直接指針兩種方式。下面讓我們看看這兩種方式吧,直接上圖:


第1張圖是通過句柄的方式對對象進(jìn)行訪問,在java堆中劃分出來一塊內(nèi)存作為句柄池,而reference中存儲的是對象的句柄地址,句柄中存儲了對象實例等信息。第2張圖是通過直接指針的方式,reference中存儲的是實例對象的地址。
這兩種對象引用的方式各有千秋,通過句柄的好處是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動的時候只會改變句柄的實例指針而reference本身不需要修改;使用直接指針的好處是速度開,不需要在java堆中在劃分出一塊內(nèi)存區(qū)域同時節(jié)省了指針定位的開銷。但是就HotSpot而言,采用的是直接指針方式。
通過以上內(nèi)容,我們明白了虛擬機中的內(nèi)存是如何劃分的,那部分區(qū)域,什么樣的代碼和操作可能導(dǎo)致內(nèi)存異常溢出異常。雖然java有垃圾收集機制,但是內(nèi)存溢出離我們并不遙遠(yuǎn),下一篇文章將講解Java垃圾收集機為避免內(nèi)存溢出異常的出現(xiàn)做了哪些努力。
以上就是Java內(nèi)存區(qū)域與內(nèi)存溢出異常相關(guān)內(nèi)容,大部分內(nèi)容直接從小臘月虛擬機相關(guān)文章直接拷貝而來(節(jié)省打字時間)。
JVM學(xué)習(xí)資料
《深入理解Java虛擬機》
Java虛擬機原理圖解系列文章
小臘月虛擬機相關(guān)文章