我們知道JVM內(nèi)存結(jié)構(gòu)也就是java程序時運行區(qū)州刽,所以在了解之前首先對其思考:
- JVM內(nèi)存結(jié)構(gòu)都包含哪幾部分,都是如何劃分的?
- 每部分都是存儲的什么數(shù)據(jù)?
- 真正運行時攻臀,一個對象創(chuàng)建的時候,是怎么分配的洪碳?
下面我們就以上面3個問題為引子架诞,來了解JVM的內(nèi)存結(jié)構(gòu)。
一.JVM的內(nèi)存結(jié)構(gòu)
如下圖所示:
可以看到狗超,運行時數(shù)據(jù)區(qū)主要包含:方法區(qū)弹澎、堆、java棧努咐、本地方法棧苦蒿、程序計數(shù)器。其中方法區(qū)和堆是所有線程共享的渗稍,java棧佩迟、本地方法棧团滥、程序計數(shù)器是線程私有的。
二报强、JVM結(jié)構(gòu)分拆
1.程序計數(shù)器(Program Counter Register)
????程序技術(shù)器是一塊較小的內(nèi)存空間灸姊,它可以看作是當(dāng)前線程所執(zhí)行的行號指示器。在任意時刻躺涝,一條 Java 虛擬機(jī)線程只會執(zhí)行一個方法的代碼厨钻,這個正在被線程執(zhí)行的方法稱為該線程的當(dāng)前方法(Current Method)。如果這個方法不是 native 的坚嗜,那 PC 寄存器就保存 Java 虛擬機(jī)正在執(zhí)行的字節(jié)碼指令的地址夯膀,如果該方法是 native 的,那 PC 寄存器的值是 undefined苍蔬。PC 寄存器的容量至少應(yīng)當(dāng)能保存一個 returnAddress 類型的數(shù)據(jù)或者一個與平臺相關(guān)的本地指針的值诱建。此內(nèi)存是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
2.java虛擬機(jī)棧
????與程序計數(shù)器一樣碟绑,Java虛擬機(jī)棧也是線程私有的俺猿,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型格仲,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表押袍、操作數(shù)棧、動態(tài)鏈接凯肋、方法出口等信息谊惭。每一個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程侮东。
我們平時所說的基本變量在棧中分配圈盔,就是說的java棧,準(zhǔn)確來說是說的局部變量表悄雅。
對于Java棧會出現(xiàn)一下2種異常情況:
- 如果線程請求的棧的深度大于虛擬機(jī)所允許的深度驱敲,將會拋出StackOverflowError異常;
- 如果 Java 虛擬機(jī)椏硐校可以動態(tài)擴(kuò)展众眨,并且擴(kuò)展的動作已經(jīng)嘗試過,但是目前無法申請到足夠的內(nèi)存去完成擴(kuò)展容诬,或者在建立新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機(jī)棧围辙,那 Java 虛擬機(jī)將會拋出一個 OutOfMemoryError 異常。
3.本地方法棧
????本地方法棧和虛擬機(jī)棧所發(fā)揮的作用非常相似放案,它們之間的區(qū)別是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),本地方法棧為虛擬機(jī)使用到的Native方法(c或c++)服務(wù)矫俺。也會拋出StackOverflowError和OutOfMemoryError異常吱殉。
4.堆(Heap)
????在 Java 虛擬機(jī)中掸冤,堆(Heap)是可供各條線程共享的運行時內(nèi)存區(qū)域,也是供所有類實例和數(shù)組對象分配內(nèi)存的區(qū)域友雳。
????Java堆是垃圾收集器管理的主要區(qū)域稿湿,所有又稱為"GC堆"(Garbage Collected Heap)。
- Java堆的內(nèi)部結(jié)構(gòu)如下:
- Java堆由新生代(YoungGeneration)和老年代(OldGeneration)構(gòu)成押赊。年輕代又分為3部分:Eden(伊甸)區(qū)饺藤、From Survivor(幸存)區(qū)、To Survivor區(qū)流礁,默認(rèn)情況下年輕代按照8:1:1的比例來分配(-XX:SurvivorRatio)涕俗。
JVM控制參數(shù):
- -Xms:JVM初始時堆內(nèi)存大小
- -Xmx:JVM最大可用的堆內(nèi)存大小
JVM也是一個軟件,也必須要獲取本機(jī)的物理內(nèi)存神帅,然后JVM會負(fù)責(zé)管理向操作系統(tǒng)申請到的內(nèi)存資源再姑。JVM啟動的時候會向操作系統(tǒng)申請 -Xms 設(shè)置的內(nèi)存,JVM啟動后運行一段時間找御,如果發(fā)現(xiàn)內(nèi)存空間不足元镀,會再次向操作系統(tǒng)申請內(nèi)存。JVM能夠獲取到的最大堆內(nèi)存是-Xmx設(shè)置的值霎桅。
- -XX:MaxNewSize設(shè)置新生代最大空間大小栖疑。
- -XX:NewSize設(shè)置新生代最小空間大小。
沒有直接設(shè)置老年代大小的參數(shù)滔驶,但可以計算出來:
老年代大杏龈铩=堆內(nèi)存大小-新生代大小
- -XX:MaxPermSize設(shè)置永久代最大空間大小瓜浸。
- -XX:PermSize設(shè)置永久代最小空間大小澳淑。
- -XX:SurvivorRatio:設(shè)置新生代中1個Eden區(qū)與1個Survivor區(qū)的大小比值,默認(rèn)設(shè)置8插佛。如新生代的大小為100M杠巡,則Eden區(qū)大小為80M,2個Survivor區(qū)大小分別為10M雇寇。
- -Xss設(shè)置每個線程的堆棧大小氢拥。
- 如果實際所需的堆超過了自動內(nèi)存管理系統(tǒng)能提供的最大容量,那 Java 虛擬機(jī)將會拋出一個OutOfMemoryError 異常锨侯。
5.方法區(qū)
????方法區(qū)用于存儲已被虛擬機(jī)加載的類信息嫩海、常量、靜態(tài)變量囚痴、即使編譯器編譯或的代碼等數(shù)據(jù)叁怪,所以它是線程共享的。雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個邏輯部分深滚,但是它卻有一個別名叫做Non-Heap(非堆)奕谭,目的應(yīng)該是與Java堆區(qū)分開來涣觉。
a.類及其父類的全限定名(java.lang.Object沒有父類)
b.類的類型(Class or Interface)
c.訪問修飾符(public, abstract, final)
d.實現(xiàn)的接口的全限定名的列表
e.常量池
f.字段信息
g.方法信息
h.靜態(tài)變量
i.ClassLoader引用
j.Class引用
????對于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)和部署程序的開發(fā)者來說,很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation)血柳,本質(zhì)上兩者并不等價官册,僅僅是因為HotSpot虛擬機(jī)的設(shè)計團(tuán)隊選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實現(xiàn)方法區(qū)而已难捌。
????Java虛擬機(jī)規(guī)范對這個區(qū)域的限制非常寬松膝宁,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實現(xiàn)垃圾收集根吁。相對而言员淫,垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了婴栽。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載满粗,一般來說這個區(qū)域的回收“成績”比較難以令人滿意,尤其是類型的卸載愚争,條件相當(dāng)苛刻映皆,但是這部分區(qū)域的回收確實是有必要的。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定轰枝,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時捅彻,將拋出OutOfMemoryError異常。
永久代變更:
- jdk1.7 HotSpot已經(jīng)字符串常量池從“永久代”移除到堆中鞍陨,String.intern詳解——參見深入理解String#intern
- jdk1.8中沒有了永久代步淹,替而代之的是:將方法區(qū)直接放在一個與堆不相連的本地內(nèi)存區(qū)域,這個區(qū)域被叫做元空間(metaspace)——java8元空間
6.運行時常量池
????運行時常量池是方法區(qū)的一部分诚撵,用于存放編譯器生成的各種字面量和符號引用缭裆。
字面量:直接給值,不聲明變量存儲寿烟。例:int a = 3; 其中a是變量澈驼,3是字面量。
符號引用:在java中筛武,一個java類將會編譯成一個class文件缝其。在編譯時,java類并不知道引用類的實際內(nèi)存地址徘六,因此只能使用符號引用來(還不知道類的具體地址内边,使用能找到該類的一個類全限定名表示)代替。比如org.simple.People類引用org.simple.Tool類待锈,在編譯時People類并不知道Tool類的實際內(nèi)存地址漠其,因此只能使用符號org.simple.Tool(假設(shè))來表示Tool類的地址。而在類裝載器裝載People類時,此時可以通過虛擬機(jī)獲取Tool類 的實際內(nèi)存地址辉懒,因此便可以既將符號org.simple.Tool替換為Tool類的實際內(nèi)存地址阳惹,即直接引用地址。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定眶俩,當(dāng)常量池?zé)o法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常快鱼。
7.直接(堆外)內(nèi)存
????直接內(nèi)存并不是虛擬機(jī)運行數(shù)據(jù)去的一部分颠印,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁的使用抹竹,也會導(dǎo)致OutOfMemoryError異常线罕。
????在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式窃判,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存钞楼,然后通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能袄琳,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)询件。——java堆外內(nèi)存詳解
三唆樊、對象訪問定位
????目前主流的訪問對象的方式是使用句柄和直接指針宛琅,2中方式如下圖所示。對于Sun HotSpot虛擬機(jī)來說逗旁,使用的直接指針訪問對象嘿辟。
????2中使用方式比較:
方式 | 優(yōu)勢 |
---|---|
句柄訪問 | reference中存儲的是句柄的地址,在對象移動(如垃圾回收時移動對象)時只會改變句柄中實例數(shù)據(jù)的指針片效,而不會改變reference的值 |
直接指針訪問 | 訪問速度快红伦,節(jié)省了一次指針定位的時間開銷 |
四、對象創(chuàng)建
????總結(jié)起來淀衣,創(chuàng)建對象分如下幾步:
1昙读、類加載檢查。檢查這個指令的參數(shù)是否能在常量池中定位到一個類的嚶嚶舌缤,并且檢查這個富豪引用的類是否已經(jīng)加載箕戳、解析、初始化国撵,如果沒有陵吸,則首先執(zhí)行類加載;
2介牙、分配內(nèi)存壮虫。類加載完成后,類所需空間大小已確定。如果Java堆的內(nèi)存是絕對規(guī)整的囚似,則使用“指針碰撞”的方式進(jìn)行分配剩拢;如果不是規(guī)整的,則是通過“空閑列表”來進(jìn)行內(nèi)存分配饶唤。