深入理解Java虛擬機讀書筆記 二

運行時數(shù)據(jù)區(qū)域

Java虛擬機所管理的內(nèi)存包括五個運行時數(shù)據(jù)區(qū)域:

運行時數(shù)據(jù)區(qū)

程序計數(shù)器

為了線程切換后能恢復(fù)到正確的執(zhí)行位置, 每條線程都需要有一個獨立的程序計數(shù)器,因而程序計數(shù)器是線程私有的內(nèi)存.此內(nèi)存區(qū)域是唯一一個在《Java虛擬機規(guī)范》 中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
如果線程正在執(zhí)行的是Java方法,計數(shù)器記錄的正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果執(zhí)行的是native方法(本地方法,原生函數(shù)),計數(shù)器的值為空.

虛擬機棧

與程序計數(shù)器一樣,它也是線程私有的,生命周期與線程相同.每個方法被執(zhí)行時,都會創(chuàng)建一個棧幀,用于存儲方法的相關(guān)信息(局部變量表等),進行入棧,當方法執(zhí)行完畢后,執(zhí)行出棧的操作.
如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常; 如果Java虛擬機棧容量可以動態(tài)擴展,當棧擴展時無法申請到足夠的內(nèi)存會拋出OutOfMemoryError異常。

本地方法棧

本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼) 服務(wù),而本地方法棧則是為虛擬機使用到的本地(Native)方法服務(wù)。
與虛擬機棧一樣, 本地方法棧也會在線程請求的棧深度大于虛擬機所允許的最大深度或者允許動態(tài)擴展時無法申請到足夠內(nèi)存而導(dǎo)致擴展失敗時分別拋出StackOverflowErrorOutOfMemoryError異常。

Java堆是被所有線程共享的內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建.此內(nèi)存區(qū)域的唯一目的就是存放對象實例,垃圾收集器管理的也是此塊區(qū)域.
如果不斷創(chuàng)建對象,并且存在GC Roots到對象之間有可達路徑(被引用)來避免垃圾回收時,會導(dǎo)致OutOfMemoryError。

方法區(qū)

方法區(qū)也是各個線程共享的內(nèi)存區(qū)域,用于存儲已被虛擬機加載的類型信息、 常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù).需要注意的是,永久代并不等同于方法區(qū)(可以不實現(xiàn)垃圾收集).
運行時常量池,用于存放類加載后Class文件中類的常量池表(通過類加載過程的loading部分),也是方法區(qū)的一部分.

可以理解為每一個類都有自身的運行時常量池,但并不意味著constant pool中所有的字段都會原樣復(fù)制一份,需要關(guān)乎到具體的jvm實現(xiàn).參照: Runtime constant pool - is filled up by variables created in runtime?

運行時常量池相對于Class常量池一大特征就是具有動態(tài)性,java規(guī)范并不要求常量只能在運行時才產(chǎn)生,也就是說運行時常量池的內(nèi)容并不全部來自Class常量池,在運行時可以通過代碼生成常量并將其放入運行時常量池中,這種特性被用的最多的就是String.intern()---拿String的內(nèi)容去StringTable(除了運行時常量池和Class常量池,還有字符串常量池)里查表,如果存在,則返回引用,不存在,就把該對象的"引用"存在StringTable表里。

需要指出的是,JDK6常量池在永久代,與堆隔離,而自JDK 7起, 原本存放在永久代的字符串常量池被移至Java堆之中.

當方法區(qū)無法滿足新的內(nèi)存分配需求時(運行時產(chǎn)生大量的類),將拋出OutOfMemoryError異常.

直接內(nèi)存

直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分.比如在JDK 1.4中新加入的NIO類,可以使用Native函數(shù)庫直接分配堆外內(nèi)存.
它會受到本機物理內(nèi)存大小的限制,因而可能會出現(xiàn)OutOfMemoryError異常(表現(xiàn)為Dump文件很小,但又使用了直接內(nèi)存,例如NIO).

虛擬機對象

HotSpot虛擬機為例,對于普通Java對象(不包括數(shù)組和Class對象)的創(chuàng)建,是從遇到new指令開始的。它首先將去檢查指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查是否已經(jīng)被加載、解析和初始化過。否則,將執(zhí)行類加載過程。
類加載完成后,將為新生對象分配內(nèi)存(類加載的準備階段只為類變量分配了內(nèi)存,實例變量在與對象實例化時一起分配內(nèi)存)。分配內(nèi)存有兩種方案:

  • 指針碰撞,適用于內(nèi)存連續(xù)的情況,比如收集器具有整理的功能
  • 空閑列表,由虛擬機維護列表記錄可用的內(nèi)存塊

分配內(nèi)存時為了解決并發(fā)的問題(避免不同的線程使用同一塊內(nèi)存給不同的對象分配),有兩種方案:

  • CAS
  • 每一個線程具有自己的內(nèi)存空間,即本地線程分配緩沖(TLAB),只在自己的緩沖區(qū)中進行分配

內(nèi)存分配完后,虛擬機會將分配到的內(nèi)存空間,除對象頭以外初始化為對應(yīng)類型的零值。因此對象的實例字段可以不賦初始值就能使用。接下來需要對對象頭進行設(shè)置,用于記錄對象的狀態(tài)。

對象包括三個部分:

  • 對象頭(Mark Word),包括兩部分:
    • 存儲對象自身運行數(shù)據(jù):根據(jù)虛擬機的位數(shù)分別為32位或者64位。例如在32位機器中,有固定的2位表示標志位,剩下的位數(shù)根據(jù)對象所處的狀態(tài)(比如輕量級鎖定、重量級鎖定等)存儲不同的內(nèi)容。
    • 類型指針:指向它的類型元數(shù)組的指針,用來確定對象是哪個類的實例。但并不是所有實現(xiàn)都會保留類型指針。比如使用句柄訪問對象。
  • 實例數(shù)據(jù):代碼里所定義的各種類型的字段內(nèi)容,包括父類繼承的
  • 對齊填充: 起到占位符的作用,并不是一定存在的。

當執(zhí)行完new指令后,會接著執(zhí)行init方法,對對象進行初始化。這樣一個對象才算是被真正構(gòu)造出來。在構(gòu)造之后,會通過棧上的reference來操作堆上的具體對象,而reference中存儲的內(nèi)容,取決于訪問對象的方式,有兩種方案:

  • 句柄訪問:會將堆中劃分出一塊內(nèi)存作為句柄池,reference存儲的是對象的句柄地址,這個句柄里包括指向堆的對象實例數(shù)據(jù)和指向方法區(qū)的對象類型數(shù)據(jù)。顯然,如果對象發(fā)生了移動,只需要改動句柄中的值,并不需要對reference進行修改

    通過句柄訪問對象

  • 直接指針訪問:reference中存儲的是對象的地址,對象中存在類型指針指向方法區(qū)的對象類型數(shù)據(jù)。這種方式節(jié)省了一次指針定位的時間開銷,速度更快。

    通過直接指針訪問對象

參考資料:
徹底弄懂java中的常量池
深入理解Java虛擬機 第二章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容