運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧、堆、方法區(qū)
線程私有的:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧
線程共享的:堆、方法區(qū)
程序計(jì)數(shù)器:
1、字節(jié)碼解釋器通過(guò)改變程序計(jì)數(shù)器來(lái)依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
2、在多線程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切換回來(lái)的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了。
Java 虛擬機(jī)棧
方法調(diào)用的數(shù)據(jù)需要通過(guò)棧進(jìn)行傳遞,每一次方法調(diào)用都會(huì)有一個(gè)對(duì)應(yīng)的棧幀被壓入棧中,每一個(gè)方法調(diào)用結(jié)束后,都會(huì)有一個(gè)棧幀被彈出。
棧由一個(gè)個(gè)棧幀組成,而每個(gè)棧幀中都擁有:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址。
本地方法棧
本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)
堆
Java 堆是所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)組都在這里分配內(nèi)存。
Java 堆還可以細(xì)分為:新生代,老年代和直接內(nèi)存
新生代: Eden 區(qū),s0、s1。
大部分情況,對(duì)象都會(huì)首先在 Eden 區(qū)域分配,在一次新生代垃圾回收后,如果對(duì)象還存活,則會(huì)進(jìn)入 S0 或者 S1,并且對(duì)象的年齡還會(huì)加 1(Eden 區(qū)->Survivor 區(qū)后對(duì)象的初始年齡變?yōu)?1),當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲),就會(huì)被晉升到老年代中。
大對(duì)象直接在老年代中分配。大對(duì)象就是需要大量連續(xù)內(nèi)存空間的對(duì)象(比如:字符串、數(shù)組)。
方法區(qū)
當(dāng)虛擬機(jī)要使用一個(gè)類時(shí),它需要讀取并解析 Class 文件獲取相關(guān)信息,再將信息存入到方法區(qū)。方法區(qū)會(huì)存儲(chǔ)已被虛擬機(jī)加載的 類信息、字段信息、方法信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。
運(yùn)行時(shí)常量池
常量池表會(huì)在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
字符串常量池
Java 程序中通常會(huì)有大量的被創(chuàng)建的字符串等待回收,將字符串常量池放到堆中,能夠更高效及時(shí)地回收字符串內(nèi)存。
對(duì)象的創(chuàng)建過(guò)程
1、類加載檢查:虛擬機(jī)遇到?條 new 指令時(shí),?先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到這個(gè)類的符號(hào)引?,并且檢查這個(gè)符號(hào)引?代表的類是否已被加載過(guò)、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)?相應(yīng)的類加載過(guò)程。
2、分配內(nèi)存:在類加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。 分配方式有 “指針碰撞” 和 “空閑列表” 兩種。
指針碰撞 :
適用場(chǎng)合 :堆內(nèi)存規(guī)整(即沒(méi)有內(nèi)存碎片)的情況下。
原理 :用過(guò)的內(nèi)存全部整合到一邊,沒(méi)有用過(guò)的內(nèi)存放在另一邊,中間有一個(gè)分界指針,只需要向著沒(méi)用過(guò)的內(nèi)存方向?qū)⒃撝羔樢苿?dòng)對(duì)象內(nèi)存大小位置即可。
使用該分配方式的 GC 收集器:Serial, ParNew
空閑列表 :
適用場(chǎng)合 : 堆內(nèi)存不規(guī)整的情況下。
原理 :虛擬機(jī)會(huì)維護(hù)一個(gè)列表,該列表中會(huì)記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候,找一塊兒足夠大的內(nèi)存塊兒來(lái)劃分給對(duì)象實(shí)例,最后更新列表記錄。
使用該分配方式的 GC 收集器:CMS
內(nèi)存分配并發(fā)問(wèn)題:
CAS+失敗重試: CAS 是樂(lè)觀鎖的一種實(shí)現(xiàn)方式。所謂樂(lè)觀鎖就是,每次不加鎖而是假設(shè)沒(méi)有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。虛擬機(jī)采用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個(gè)線程預(yù)先在 Eden 區(qū)分配一塊兒內(nèi)存,JVM 在給線程中的對(duì)象分配內(nèi)存時(shí),首先在 TLAB 分配,當(dāng)對(duì)象大于 TLAB 中的剩余內(nèi)存或 TLAB 的內(nèi)存已用盡時(shí),再采用上述的 CAS 進(jìn)行內(nèi)存分配。
3、初始化零值:內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),這?步操作保證了對(duì)象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使?,程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
4、設(shè)置對(duì)象頭:初始化零值完成之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)?必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息。 這些信息存放在對(duì)象頭中。
5、執(zhí)?init?法:執(zhí)? new 指令之后會(huì)接著執(zhí)? ?法,把對(duì)象按照程序員的意愿進(jìn)?初始化,這樣?個(gè)真正可?的對(duì)象才算完全產(chǎn)?出來(lái)。
對(duì)象的內(nèi)存布局
對(duì)象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),另一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序中所定義的各種類型的字段內(nèi)容。
對(duì)齊填充部分不是必然存在的,也沒(méi)有什么特別的含義,僅僅起占位作用。
對(duì)象的訪問(wèn)定位
目前主流的訪問(wèn)方式有:使用句柄、直接指針。