JVM運行時內(nèi)存數(shù)據(jù)區(qū)域
前言
JVM會在執(zhí)行過程中把它所管理的內(nèi)存花費為若干個不同的數(shù)據(jù)區(qū)域。如下圖所示

下面分別對這些區(qū)域進行解釋。
1、程序技術(shù)器
- 概念:程序技術(shù)器是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。
- 作用:
- 字節(jié)碼解釋器工作時通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
- JVM的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方法實現(xiàn)的。在任意一個時刻,一個內(nèi)核都只會執(zhí)行一條線程中的指令。因此為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個獨立的程序計數(shù)器,線程之間的技術(shù)器互不影響,獨立存儲。因此這類內(nèi)存區(qū)域是線程私有的。
- 其他:如果線程執(zhí)行的是一個java方法,這個技術(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的是Native方法,這個計數(shù)器為空,此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError的地方。
2、Java虛擬機棧
- 概念:虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量,操作數(shù)棧,動態(tài)鏈接,方法出口等消息。每一個方法從調(diào)用至執(zhí)行完成的過程,對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
- 生命周期: 與其線程相同。
- 是否線程私有:是
- 局部變量表:存放了編譯器可知的基本數(shù)據(jù)類型,對象引用和指向一條字節(jié)碼指令的地址。其中64位長度的long和double占用2個局部變量空間,其余類型只占有一個。
- 異常:
- StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度。換言之線程請求的棧容量超過棧允許的最大容量。常見的就是遞歸調(diào)用沒有正確終止,導(dǎo)致棧溢出。
- OutOfMemoryError:虛擬機棧動態(tài)擴展時如果無法申請到足夠的內(nèi)存空間,就會拋出OutOfMemoryError異常。
3、本地方法棧
- 概念:本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的。兩者之間的區(qū)別在于虛擬機棧為虛擬機執(zhí)行java方法服務(wù),而本地方法棧則為虛擬機使用到的Native方法服務(wù)。其他均是一樣的。
4、Java堆
- 概念:java Heap 是java虛擬機所管理的內(nèi)存中最大的一塊。
- 是否線程私有: 否,所有線程共享的一塊內(nèi)存區(qū)域。
- 生命周期:在虛擬機啟動的時候創(chuàng)建。
- 目的與作用:存放對象實例,幾乎所有的對象實例都是在這里分配內(nèi)存。是垃圾收集器管理的主要區(qū)域。
- 細(xì)分:從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為新生代,老年代。再細(xì)致可以分為Eden空間,F(xiàn)rom Survivor空間,To Survivor空間等。

- 參數(shù)控制:
-Xms JVM啟動時申請的最小堆內(nèi)存,默認(rèn)為物理內(nèi)存的1/64但小于1G
-Xmx JVM啟動時申請的最大堆內(nèi)存,默認(rèn)為物理內(nèi)存但1/4但小于1G
-XX:MinHeapFreeRadio 默認(rèn)當(dāng)剩余堆內(nèi)存空間小于40%時,JVM會將-Xms會增大到-Xmx的大小,通過該參數(shù)可以指定這個比例
-XX:MaxHeapFreeRadio 默認(rèn)當(dāng)空余堆內(nèi)存大于70%時,JVM會減小堆內(nèi)存至-Xms大小,通過該參數(shù)可以指定這個比例
5、方法區(qū)
- 概念:方法區(qū)存放了要加載的類的消息,類中的靜態(tài)變量,final定義的常量,類中的field方法信息,對象中的getName,isInterface等方法的所需數(shù)據(jù)均是來源于方法區(qū)。
- 是否線程私有:否
- 對于HotSpot虛擬機來說,很多人更愿意將方法區(qū)稱為永久代,這因為GC分代收集擴展至方法區(qū),或者說是使用永久代(Permanent Generation)實現(xiàn)方法區(qū)。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收(字符常量池已經(jīng)移出永久代)和類型的卸載。永久代在java8中已經(jīng)被完全移除,原先永久代中類的元信息會被放入本地內(nèi)存(元數(shù)據(jù)區(qū),metaspace),將類的靜態(tài)變量和內(nèi)部字符串放入java堆中。
- Metaspace:默認(rèn)情況下,類元數(shù)據(jù)只受可用的本地內(nèi)存限制,通過參數(shù) -XX:MaxMetaspaceSize可以限制本地內(nèi)存分配給類元數(shù)據(jù)的大小,若沒有指定這個參數(shù),元空間會在運行時根據(jù)需要動態(tài)調(diào)整。對于僵死的類及加載器的垃圾回收將在元數(shù)據(jù)使用達到MaxMetaspaceSize時進行GC。
- 異常:OutOfMemoryError異常。 當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時會拋出該異常。
6、運行時常量池
- 運行時常量池是方法區(qū)的一部分。主要用于存放編譯期生成的各種字面量和符號引用。這部分內(nèi)容將會在類加載后進入方法區(qū)的運行時常量池中存放。并不是要求常量一定只用在編譯的時候才能纏手,運行期間也可能將新的常量放入池中,例如String.intern()方法。
- 異常:當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出OutOfMemoryError。
7、直接內(nèi)存
直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也會被頻繁調(diào)用,而且可能導(dǎo)致OutOfMemoryError異常。
在NIO類中引入了基于Channel與Buffer的I/O方式,其可以使用Native函數(shù)庫直接分配堆外內(nèi)存。然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作,這樣避免了在Java堆與Native堆中來回復(fù)制數(shù)據(jù)。
直接內(nèi)存的分配不會受到Java堆大小的限制,但是內(nèi)存一定會受到本機總內(nèi)存大小以及處理器尋址空間的限制。千萬不要在設(shè)置-Xmx時忽略直接內(nèi)存,從而使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制,導(dǎo)致在進行動態(tài)擴展時出現(xiàn)OutOfMemoryError異常
對象探秘
對象的創(chuàng)建
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用是否已經(jīng)被加載、解析和初始化過。若沒有則先執(zhí)行類加載過程。在類加載通過后,虛擬機將為新生對象分配內(nèi)存,相當(dāng)于是從Java堆中劃出來一部分。目前有兩種分配方法。
- 指針碰撞。要求Java堆內(nèi)存中是絕對規(guī)整的,所用用過的內(nèi)存在一邊,空閑的在另一邊,中獎放著一個指針作為分界點的指示器。分配內(nèi)存就是把指針向空閑空間那邊挪動一段與對象大小相等的距離。
- 空閑列表。堆內(nèi)存并不是規(guī)整的,使用的內(nèi)存和空閑的內(nèi)存相互交錯,虛擬機維護了一個列表,記錄了哪些內(nèi)存塊是可用的。
Java堆是否規(guī)整,取決于垃圾收集器是否帶有壓縮整理功能決定的。因此在使用Serial,ParNew等帶Compact過程的收集器時,采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用的是空閑列表。
由于修改指針?biāo)赶虻膬?nèi)存地址在并發(fā)情況下,不是線程安全的。目前有兩種方案解決這個問題。
- 對分配內(nèi)存空間的動作進行同步處理,虛擬機采用CAS配上失敗重試的方法保證更新的原子性。
- 把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在java堆中預(yù)先分配一塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer ,TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的TLAB上,只用TLAB用完,分配新的TLAB時,才需要同步鎖定。
內(nèi)存分配完成后,虛擬機將內(nèi)存空間初始化為0,然后對對象進行必要的設(shè)置,設(shè)置信息存放在對象頭。最后調(diào)用init方法得到一個可用的對象。
對象的內(nèi)存布局
對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭,實例數(shù)據(jù)和對齊填充。
對象頭
對象頭信息分為兩部分.
- 第一部分用于存儲對象自身的運行時數(shù)據(jù),如哈希碼,GC分代年齡,鎖狀態(tài)標(biāo)志,線程持有的鎖,偏向線程ID,偏向時間戳等。
| 存儲內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
|---|---|---|
| 對象哈希碼,對象分代年齡 | 01 | 未鎖定 |
| 指向鎖記錄的指針 | 00 | 輕量級鎖定 |
| 指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
| 空,不需要記錄信息 | 11 | GC標(biāo)記 |
| 偏向線程ID,偏向時間戳,對象分代年齡 | 01 | 可偏向 |
- 對象頭的另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
- 對齊填充不是必然存在的,也沒有特別的含義。
對象的訪問定位
Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種。
-
使用句柄訪問,在java堆中會劃分出一塊內(nèi)存用來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中則包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
句柄訪問 -
使用地址直接訪問,此時reference中存儲的直接就是對象的地址。
地址訪問 句柄的優(yōu)勢:reference中存儲的是穩(wěn)定的句柄地址,在對象的位置發(fā)生改變的時候,只會改變句柄中的實例數(shù)據(jù)指針,而reference不會受到影響。
直接指針訪問優(yōu)勢:速度更快,減少了一個指針定位的時間開銷。

