JVM運行時內存區(qū)域劃分

JVM 內存區(qū)域

JVM會將Java進程所管理的內存劃分為若干不同的數據區(qū)域. 這些區(qū)域有各自的用途、創(chuàng)建/銷毀時間:

線程私有區(qū)域

線程私有數據區(qū)域生命周期與線程相同,依賴用戶線程的啟動/結束而創(chuàng)建/銷毀(在Hotspot VM內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區(qū)域的存/否跟隨本地線程的生/死).

1. Program Counter Register(程序計數器):

一塊較小的內存空間,作用是當前線程所執(zhí)行字節(jié)碼的行號指示器(類似于傳統CPU模型中的PC), PC在每次指令執(zhí)行后自增, 維護下一個將要執(zhí)行指令的地址. 在JVM模型中, 字節(jié)碼解釋器就是通過改變PC值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉、異常處理、線程恢復等基礎功能都需要依賴PC完成(僅限于Java方法, Native方法該計數器值為undefined).

不同于OS以進程為單位調度, JVM中的并發(fā)是通過線程切換并分配時間片執(zhí)行來實現的. 在任何一個時刻, 一個處理器內核只會執(zhí)行一條線程中的指令. 因此, 為了線程切換后能恢復到正確的執(zhí)行位置, 每條線程都需要有一個獨立的程序計數器, 這類內存被稱為“線程私有”內存.

2. Java Stack(虛擬機棧)

虛擬機棧描述的是Java方法執(zhí)行的內存模型: 每個方法被執(zhí)行時會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態(tài)鏈接、方法出口等信息. 每個方法被調用至返回的過程, 就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程(VM提供了-Xss來指定線程的最大??臻g, 該參數也直接決定了函數調用的最大深度).

局部變量表(對應我們常說的‘堆?!械摹畻!?存放了編譯期可知的各種基本數據類型(如boolean、int、double等) 、對象引用(reference : 不等同于對象本身, 可能是一個指向對象起始地址的指針, 也可能指向一個代表對象的句柄或其他與此對象相關的位置, 見下: HotSpot對象定位方式) 和 returnAddress類型(指向一條字節(jié)碼指令的地址). 其中l(wèi)ong和double占用2個局部變量空間(Slot), 其余只占用1個. 如下Java方法代碼可以使用javap命令或javassist等字節(jié)碼工具讀到:

public String test(int a, long b, float c, double d, Date date, List list) {

????StringBuilder?sb = new StringBuilder().append(a).append(b).append(c).append(d).append(date);

????for (String str : list) {

????????sb.append(str);

????}

????return sb.toString();

}

注: javap/javassist讀到的其實是靜態(tài)數據, 而局部變量表內存儲的卻是運行時動態(tài)加載的動態(tài)數據, 但因為局部變量表所需的內存空間在編譯期間即可完成分配, 當進入一個方法時, 這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間大小不會改變, 因此可以在概念上認定這兩部分內容存儲的數據格式相同.

3.Native Method Stack(本地方法棧)

與Java Stack作用類似, 區(qū)別是Java Stack為執(zhí)行Java方法服務, 而本地方法棧則為Native方法服務, 如果一個VM實現使用C-linkage模型來支持Native調用, 那么該棧將會是一個C棧, 但HotSpot VM直接就把本地方法棧和虛擬機棧合二為一.

線程共享區(qū)域

隨虛擬機的啟動/關閉而創(chuàng)建/銷毀.

1. Heap(Java堆)

幾乎所有對象實例和數組都要在堆上分配(棧上分配、標量替換除外), 因此是JVM管理的最大一塊內存, 也是垃圾收集器的主要活動區(qū)域. 由于現代JVM采用分代收集算法, 因此Java堆從GC的角度還可以細分為: 新生代(Eden區(qū)、From Survivor區(qū)和To Survivor區(qū))和老年代; 而從內存分配的角度來看, 線程共享的Java堆還還可以劃分出多個線程私有的分配緩沖區(qū)(TLAB). 而進一步劃分的目的是為了更好地回收內存和更快地分配內存.

2. Method Area(方法區(qū))

即我們常說的永久代(Permanent Generation),用于存儲被JVM加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數據. HotSpot VM把GC分代收集擴展至方法區(qū), 即使用Java堆的永久代來實現方法區(qū), 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存, 而不必為方法區(qū)開發(fā)專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)

不過在1.7的HotSpot已經將原本放在永久代的字符串常量池移出:

而在1.8中, 永久區(qū)已經被徹底移除, 取而代之的是元數據區(qū)Metaspace(這一點在查看GC日志和使用jstat -gcutil查看GC情況時可以觀察到),與永久代不同, 如果不指定Metaspace大小, 如果方法區(qū)持續(xù)增長, JVM會默認耗盡所有系統內存.

運行時常量池

方法區(qū)的一部分. Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項常量池(Constant Pool Table)用于存放編譯期生成的各種字面量和符號引用, 這部分內容會存放到方法區(qū)的運行時常量池中(如前面從test方法中讀到的signature信息). 但Java語言并不要求常量一定只能在編譯期產生, 即并非預置入Class文件中常量池的內容才能進入方法區(qū)運行時常量池, 運行期間也可能將新的常量放入池中, 如String的intern()方法.

直接內存

直接內存并不是JVM運行時數據區(qū)的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基于Channel與Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然后使用DirectByteBuffer對象作為這塊內存的引用進行操作, 這樣就避免了在Java堆和Native堆中來回復制數據, 因此在一些場景中可以顯著提高性能.

顯然,本機直接內存的分配不會受到Java堆大小的限制(即不會遵守-Xms、-Xmx等設置), 但既然是內存, 則肯定還是會受到本機總內存大小及處理器尋址空間的限制, 因此動態(tài)擴展時也會出現OutOfMemoryError異常.

HotSpot對象

對象新建

new一個Java Object(包括數組和Class對象), 在JVM會發(fā)生如下步驟:

JVM遇到new指令: 首先去檢查該指令的參數是否能在常量池中定位到一個類的符號引用, 并檢查這個符號引用代表的類是否已被加載、解析和初始化過. 如果沒有, 必須先執(zhí)行相應的類加載過程.

類加載檢查通過后:JVM將為新生對象分配內存(對象所需內存的大小在類加載完成后便可完全確定), JVM采用指針碰撞(內存規(guī)整: Serial、ParNew等有內存壓縮整理功能的收集器)或空閑鏈表(內存不規(guī)整: CMS這種基于Mark-Sweep算法的收集器)方式將一塊確定大小的內存從Java堆中劃分出來.

除了考慮如何劃分可用空間外,由于在JVM上創(chuàng)建對象的行為非常頻繁, 因此需要考慮內存分配的并發(fā)問題. 解決方案有兩個:

對分配內存空間的動作進行同步-采用 CAS配上失敗重試 方式保證更新操作的原子性;

把內存分配的動作按照線程劃分在不同的空間之中進行-每個線程在Java堆中預先分配一小塊內存, 稱為本地線程分配緩沖TLAB, 各線程首先在TLAB上分配, 只有TLAB用完, 分配新的TLAB時才需要同步鎖定(使用-XX:+/-UseTLAB參數設定).

接下來將分配到的內存空間初始化為零值(不包括對象頭, 且如果使用TLAB這一個工作也可以提前至TLAB分配時進行). 這一步保證了對象的實例字段可以不賦初始值就直接使用(訪問到這些字段的數據類型所對應的零值).

然后要對對象進行必要的設置:如該對象所屬的類實例、如何能訪問到類的元數據信息、對象的哈希碼、對象的GC分代年齡等, 這部分息放在對象頭中(詳見下).

上面工作都完成之后,在虛擬機角度一個新對象已經產生, 但在Java視角對象的創(chuàng)建才剛剛開始(方法尚未執(zhí)行, 所有字段還都為零). 所以new指令之后一般會(由字節(jié)碼中是否跟隨有invokespecial指令所決定-Interface一般不會有, 而Class一般會有)接著執(zhí)行方法, 把對象按照程序員的意愿進行初始化, 這樣一個真正可用的對象才算完全產生出來.

對象存儲布局

HotSpot VM內, 對象在內存中的存儲布局可以分為三塊區(qū)域:對象頭、實例數據和對齊填充:

對象頭包括兩部分:

一部分是類型指針,即是對象指向它的類元數據的指針: JVM通過該指針確定該對象屬于哪個類實例. 另外, 如果對象是一個數組, 那在對象頭中還必須有一塊數據用于記錄數組長度.

注意:并非所有JVM實現都必須在對象數據上保留類型指針, 也就是說查找對象的元數據并非一定要經過對象本身(詳見下面句柄定位對象方式).

一部分用于存儲對象自身的運行時數據: HashCode、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等, 這部分數據的長度在32位和64位的JVM(暫不考慮開啟壓縮指針)中分別為32bit和64bit, 官方稱之為“Mark Word”; 其存儲格式如下:

實例數據部分是對象真正存儲的有效信息,也就是我們在代碼里所定義的各種類型的字段內容(無論是從父類繼承下來的, 還是在子類中定義的都需要記錄下來). 這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響. HotSpot默認的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers), 相同寬度的字段總是被分配到一起, 在滿足這個前提條件下, 在父類中定義的變量會出現在子類之前. 如果CompactFields參數值為true(默認), 那子類中較窄的變量也可能會插入到父類變量的空隙中.

對齊填充部分并不是必然存在的,僅起到占位符的作用, 原因是HotSpot自動內存管理系統要求對象起始地址必須是8字節(jié)的整數倍, 即對象的大小必須是8字節(jié)的整數倍.

對象定位

建立對象是為了使用對象, Java程序需要通過棧上的reference來操作堆上的具體對象. 主流的有句柄和直接指針兩種方式去定位和訪問堆上的對象:

句柄: Java堆中將會劃分出一塊內存來作為句柄池, reference中存儲對象的句柄地址, 而句柄中包含了對象實例數據與類型數據的具體各自的地址信息:

直接指針(HotSpot使用): 該方式Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息, reference中存儲的直接就是對象地址:

這兩種對象訪問方式各有優(yōu)勢:使用句柄來訪問的最大好處是reference中存儲的是穩(wěn)定句柄地址, 在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不變. 而使用直接指針最大的好處就是速度更快, 它節(jié)省了一次指針定位的時間開銷,由于對象訪問非常頻繁, 因此這類開銷積小成多也是一項非??捎^的執(zhí)行成本.

四、Java堆內存的10個要點

Java堆內存是操作系統分配給JVM的內存的一部分。

當我們創(chuàng)建對象時,它們存儲在Java堆內存中。

為了便于垃圾回收,Java堆空間分成三個區(qū)域,分別叫作New Generation, Old Generation或叫作Tenured Generation,還有Perm Space。

你可以通過用JVM的命令行選項 -Xms, -Xmx, -Xmn來調整Java堆空間的大小。不要忘了在大小后面加上”M”或者”G”來表示單位。舉個例子,你可以用 -Xmx256m來設置堆內存最大的大小為256MB。

你可以用JConsole或者 Runtime.maxMemory(), Runtime.totalMemory(), Runtime.freeMemory()來查看Java中堆內存的大小。

你可以使用命令“jmap”來獲得heap dump,用“jhat”來分析heap dump。

Java堆空間不同于??臻g,棧空間是用來儲存調用棧和局部變量的。

Java垃圾回收器是用來將死掉的對象(不再使用的對象)所占用的內存回收回來,再釋放到Java堆空間中。

當你遇到java.lang.outOfMemoryError時,不要緊張,有時候僅僅增加堆空間就可以了,但如果經常出現的話,就要看看Java程序中是不是存在內存泄露了。

請使用Profiler和Heap dump分析工具來查看Java堆空間,可以查看給每個對象分配了多少內存。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容