Java 虛擬機(jī)

參考:
https://mp.weixin.qq.com/s/oDeO8Td-SJn9g4mRygqtSw

JVM 運(yùn)行時(shí)的內(nèi)存結(jié)構(gòu)

jvm_mem.png

1. Heap

內(nèi)存區(qū)域中最大的一塊區(qū)域,被所有線程共享,存儲(chǔ)著幾乎所有的實(shí)例對(duì)象、數(shù)組。

堆大小設(shè)置

根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。在實(shí)現(xiàn)時(shí),既可以實(shí)現(xiàn)成固定大小的,也可以在運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整。

-Xms-Xmx 設(shè)置初始值和最大值,如 -Xms256M -Xmx1024M. ms 是 memory start,mx 是 memory max.

通常情況下,服務(wù)器在運(yùn)行過程中,堆空間不斷地?cái)U(kuò)容與回縮,會(huì)形成不必要的系統(tǒng)壓力。因此線上生產(chǎn)環(huán)境中 JVM 的 Xms 和 Xmx 會(huì)設(shè)置成同樣大小,避免在 GC 后調(diào)整堆大小時(shí)帶來的額外壓力。

堆內(nèi)部結(jié)構(gòu)

默認(rèn)堆空間分配:Young Generation 為 1/3, Old Generation 為 2/3. 其中,Young Generation 中,Eden 占 8/10, Suivivor0 和 Survivor1 均為 1/10.

相關(guān)參數(shù):

  • -XX:InitialSurvivorRatio, 表示 Eden 與 Survivor 的比例,默認(rèn)為 8 表示 Eden 與兩個(gè) Survivor 比例為 8:1:1
  • -XX:NewRatio, 表示 Old Generation 與 Young Generation 的比例,默認(rèn)為 2 表示 2:1.

新對(duì)象內(nèi)存分配流程

絕大部分對(duì)象在 Eden 區(qū)生成,當(dāng) Eden 區(qū)裝填滿的時(shí)候,會(huì)觸發(fā) Young Garbage Collection,即 YGC。垃圾回收的時(shí)候,在 Eden 區(qū)實(shí)現(xiàn)清除策略,沒有被引用的對(duì)象則直接回收。依然存活的對(duì)象會(huì)被移送到Survivor 區(qū)。Survivor 區(qū)分為 so 和 s1 兩塊內(nèi)存空間。每次 YGC 的時(shí)候,它們將存活的對(duì)象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除,交換兩塊空間的使用狀態(tài)。如果 YGC 要移送的對(duì)象大于 Survivor 區(qū)容量的上限,則直接移交給老年代。一個(gè)對(duì)象也不可能永遠(yuǎn)呆在新生代,就像人到了 18 歲就會(huì)成年一樣,在 JVM 中 -XX:MaxTenuringThreshold 參數(shù)就是來配置一個(gè)對(duì)象從新生代晉升到老年代的閾值。默認(rèn)值是 15,可以在 Survivor 區(qū)交換 14 次之后,晉升至老年代。

2. Metaspace

存放類和方法的元數(shù)據(jù)以及常量池。元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。

Java 8 中 PermGen 被移出 HotSpot JVM, 主要原因是避免 java.lang.OutOfMemoryError: PermGen 以及更好地與 JRockit VM 融合。

Java 7 字符串常量池被從方法區(qū)拿到了堆,Java 8 時(shí),PermGen 被元空間代替,其他內(nèi)容比如類元信息、字段、靜態(tài)屬性、方法、常量等都移動(dòng)到元空間區(qū)。

3. JVM Stacks

每個(gè) Java 方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過程,對(duì)應(yīng)著一個(gè)棧幀在 Java 虛擬機(jī)棧中入棧和出棧的過程。

虛擬機(jī)棧的生命周期和線程是一致的,并且是線程私有的。

StackOverflowError 表示請(qǐng)求的棧溢出, 導(dǎo)致內(nèi)存耗盡, 通常出現(xiàn)在遞歸方法中。

棧幀(Stack Frame)包含局部變量表,操作棧(Operand Stack),動(dòng)態(tài)連接,方法返回地址。

可以通過 -Xss 這個(gè)虛擬機(jī)參數(shù)來指定每個(gè)線程的 Java 虛擬機(jī)棧內(nèi)存大小。JDK 5.0以后每個(gè)線程堆棧大小為1M, 以前每個(gè)線程堆棧大小為256K.更具應(yīng)用的線程所需內(nèi)存大小進(jìn)行 調(diào)整.在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程.但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的, 不能無限生成,經(jīng)驗(yàn)值在3000~5000左右。

該區(qū)域可能拋出以下異常:

  • 當(dāng)線程請(qǐng)求的棧深度超過最大值,會(huì)拋出 StackOverflowError 異常;
  • 棧進(jìn)行動(dòng)態(tài)擴(kuò)展時(shí)如果無法申請(qǐng)到足夠內(nèi)存,會(huì)拋出 OutOfMemoryError 異常

4. Native Method Stacks

有的虛擬機(jī)(譬如 Sun HotSpot 虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。

5. Program Counter Register

記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址(如果正在執(zhí)行的是本地方法則為空)。可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。

代碼是在線程中運(yùn)行的,線程有可能被掛起。即 CPU 一會(huì)執(zhí)行線程 A,線程 A 還沒有執(zhí)行完被掛起了,接著執(zhí)行線程 B,最后又來執(zhí)行線程 A 了,CPU 得知道執(zhí)行線程A的哪一部分指令,線程計(jì)數(shù)器會(huì)告訴 CPU。

為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。每個(gè)線程在創(chuàng)建后,都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器和棧幀,程序計(jì)數(shù)器用來存放執(zhí)行指令的偏移量和行號(hào)指示器等,線程執(zhí)行或恢復(fù)都要依賴程序計(jì)數(shù)器。此區(qū)域也不會(huì)發(fā)生內(nèi)存溢出異常。

6. Direct Memory

在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。如果內(nèi)存區(qū)域總和大于物理內(nèi)存的限制,也會(huì)出現(xiàn) OOM。

7. Code Cache

JVM 代碼緩存是 JVM 將其字節(jié)碼存儲(chǔ)為本機(jī)代碼的區(qū)域。實(shí)時(shí)(JIT)編譯器是代碼緩存區(qū)域的最大消費(fèi)者。這就是為什么一些開發(fā)人員將此內(nèi)存稱為 JIT 代碼緩存的原因。

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

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

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