基於JDK1.8的JVM 內存結構【JVM篇三】


在我的上一篇文章別翻了,這篇文章絕對讓你深刻理解java類的加載以及ClassLoader源碼分析【JVM篇二】中,相信大家已經對java類加載機制有一個比較全面的理解了,那么類加載之后,字節碼數據在 Java 虛擬機內存中是如何存放的 ?Java 虛擬機在為類實例或成員變量分配內存是如何分配的 ?是的,這兩個問題就涉及到了JVM 內存結構的知識了,那么這篇文章將進行解答。

@

1、內存結構還是運行時數據區?

要解答本篇上面的這些問題,我們首先需要了解一下 Java 虛擬機的內存結構。

從某一角度來說,Java 虛擬機的內存結構 == 運行時數據區,在《Java 虛擬機規范》中用的是【運行時數據區】術語的,並沒有內存結構這么一說法。內存結構只是聽着更加貼切,更加形象,因此知道內存結構就是運行時數據區的意思就好了!也沒必要鑽牛角尖糾結這個問題~

2、運行時數據區

JVM被分為三個主要的子系統:類加載器子系統、運行時數據區和執行引擎 。而今天的這篇文章主要講解其中的運行時數據區(Runtime Data Areas)
在這里插入圖片描述
在 Java 虛擬機規范中,定義了五種運行時數據區,分別是 Java 、方法區、虛擬機、本地方法區、程序計數器 !

順道提一句運行時常量池也會進入方法區,也就是說方法區中就已經包括了常量池。

特別注意其中Java 堆和方法區是 線程共享的。其他都是 線程私有的。

在這里插入圖片描述

3、線程共享:Java堆、方法區

我們首先來了解了解一下線程共享的Java堆和方法區!

3.1、Java堆

Java 堆是所有線程共享的,它在虛擬機啟動時就會被創建

Java 堆是內存空間占據的最大一塊區域了,Java 堆是用來存放對象實例數組,也就是說我們代碼中通過 new 關鍵字 new 出來的對象都存放在這里。所以這里也就成為了垃圾回收器的主要活動營地了,於是它就有了一個別名叫做 GC 堆,並且單個 JVM 進程有且僅有一個 Java 堆。根據垃圾回收器的規則,我們可以對 Java 堆進行進一步的划分,具體 Java 堆內存結構如下圖所示:
在這里插入圖片描述
從上圖可以看出Java 堆並不是單純的一整塊區域,實際上java堆是根據對象存活時間的不同,Java 堆還被分為年輕代、老年代兩個區域,年輕代還被進一步划分為 Eden 區、From Survivor 0、To Survivor 1 區。並且默認的虛擬機配置比例是Eden:from :to = 8:1:1 。簡單來說就是:

Java堆 = 老年代 + 新生代

 

新生代 = Eden + S0 + S1

 

默認Eden:from :to = 8:1:1

仔細看過上面的 Java 堆結構圖童鞋可能會發現了-Xms和-Xmn的字樣,是的這個正是控制堆的JVM的參數,實際上我們是可以通過JVM參數動態控制 Java 堆中的各空間大小的,關於JVM的參數是有很多的,但是常用的也就那么幾個,不多的,用的多了都會很容易記住的,下面我們來講講關於堆的JVM常見的參數:

-Xms: 堆容量初始大小(堆包括新生代和老年代)。 例如:-Xms 20M
-Xmx: 堆總共(最大)大小。 例如:-Xmx 30M
注意:建議將 -Xms 和 -Xmx 設為相同值,避免每次垃圾回收完成后JVM重新分配內存!
-Xmn: 新生代容量大小。例如:-Xmn 10M
-XX: SurvivorRatio 設置參數Eden、form和to的比例 【比例參數Eden、form和to默認是8:1:1】例如:-XX: SurvivorRatio=8 代表比例8:1:1

 

雖然沒有直接設置老年代的參數,但是可以設置堆空間大小和新生代空間大小兩個參數來間接控制:
老年代空間大小 = 堆空間大小 - 年輕代大空間大小

當我們的 Java 堆內有足夠的空間去完成實例分配時,並且堆也無法擴展,將會拋出我們常見的OutOfMemoryError 異常,也就是我們常說的OOM 異常

3.2、 JVM 堆內存溢出后,其他線程是否可繼續工作?

JVM 堆內存溢出后也就是OOM 異常,網上有一道非常火的面試題:JVM 堆內存溢出后,其他線程是否可繼續工作?

實際上這個問題需要具體的場景分析。但是就一般情況下,發生OOM的線程都會終結(除非代碼寫的太爛),該線程持有的對象占用的heap都會被gc了,釋放內存。因為發生OOM之前要進行gc,就算其他線程能夠正常工作,也會因為頻繁gc產生較大的影響。

也就是說發生OOM的線程一般情況下會死亡,也就是會被終結掉,該線程持有的對象占用的heap都會被gc了,釋放內存。因為發生OOM之前要進行gc,就算其他線程能夠正常工作,也會因為頻繁gc產生較大的影響。

3.3、方法區

拿HotSpot 虛擬機來說,在 JDK1.7的時候,方法區被稱作為永久代, 從JDK1.8開始,Metaspace (元空間)也就是我們所謂的方法區!

 

也就是說,如果你身邊的小伙伴還在說着永久代,那絕壁是在扯1.8之前的概念了,1.8之后已經廢棄了永久代這個概念!

方法區(Method Area)與上面講的Java堆一樣,都是各個線程共享的,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

Java虛擬機規范中是這樣定義方法區的:
它存儲了每個類的結構信息,例如運行時常量池、字段、方法數據、構造函數和普通方法的字節碼內容,還包括一些在類、實例、接口初始化時用到的特殊方法。

3.4、JDK1.8 之前的方法區

就以HotSpot 虛擬機來說,在 JDK1.8 之前,方法區也被稱作為永久代,這個方法區會發生我們常見的 java.lang.OutOfMemoryError: PermGen space 異常,注意是永久代異常信息,我們也可以通過啟動參數來控制方法區的大小:

-XX:PermSize 設置方法區最小空間
-XX:MaxPermSize 設置方法區最大空間

在JDK7之前的HotSpot虛擬機中,納入字符串常量池的字符串被存儲在永久代中,因此導致了一系列的性能問題和內存溢出錯誤。特別突出的例子就是Stringintern()方法

3.5、JDK1.8 之后的方法區

JDK8之后就沒有永久代這一說法變成叫做元空間(meta space),而且將老年代與元空間剝離。元空間放置於本地的內存中,因此元空間的最大空間就是系統的內存空間了,從而不會再出現像永久代的內存溢出錯誤了,也不會出現泄漏的數據移到交換區這樣的事情。用戶可以為元空間設置一個可用空間最大值,不設置默認根據類的元數據大小動態增加元空間的容量。對於一個 64 位的服務器端 JVM 來說,其默認的–XX:MetaspaceSize 值為 21MB。也就是說默認的元空間大小是21MB

只要類加載器還存活,其加載的類的元數據也是存活的,不會被回收掉!也就是同生共死

在這里插入圖片描述

3.6、JDK1.8 之后的方法區為何變化如此之大?

做這個改變呢也許主要是基於以下兩點原因:

1、由於 永久代(PermGen)內存經常會溢出,引發惱人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的開發者希望這一塊內存可以更靈活地被管理,不要再經常出現這樣的 OOM錯誤。

2、移除 永久代(PermGen)可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。

還有需要注意一點的是永久代的移除並不代表自定義的類加載器泄露問題就解決了。因此,你還必須監控你的內存消耗情況,因為一旦發生泄漏,會占用你的大量本地內存,並且還可能導致交換區交換更加糟糕。

4、線程私有:程序計數器、Java 虛擬機棧、本地方法棧

Java 堆以及方法區的數據是共享的,但是有一些部分則是線程私有的。線程私有部分可以分為:程序計數器、Java 虛擬機棧、本地方法棧三大部分。

4.1、Java 虛擬機棧(JVM Stacks)

1、 Java 虛擬機的每一條線程都有自己私有的 Java 虛擬機棧,這個 Java 虛擬機棧跟線程同時創建,所以它跟線程有相同的生命周期。

2、Java 虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中的入棧到出棧的過程

3、局部變量表存放了編譯期可知的各種基本數據類型、對象引用和 returnAddress 類型。

1、基本類型:八種基本類型
2、對象引用:reference 類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置。
3、 returnAddress 類型:指向了一條字節碼指令的地址。

其中 64 位長度的 long 和 double 類型的數據會占用 2 個局部變量空間(Slot),其余的數據類型只占用 1 個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

4、Java 虛擬機棧既允許被實現成固定的大小,也允許根據計算動態來擴展和收縮,如果采用固定大小的話,每一個線程的 Java 虛擬機棧容量可以在線程創建的時候獨立選定。在 Java 虛擬機棧中會發生兩種異常,這個在虛擬機規范中有指出:

  • 如果線程請求分配的棧容量超過 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出 StackOverflowError 異常;也就是棧溢出錯誤!方法遞歸調用產生StackOverflowError 異常這種結果。
  • 如果 Java 虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存或者在創建新的線程時沒有足夠的內存去創建對應的 Java 虛擬機棧,那么虛擬機將會拋出 OutOfMemoryError 異常。也就是OOM內存溢出錯誤!(線程啟動過多)

當然,可以通過參數 -Xss 去調整JVM棧的大小!

4.2、本地方法棧(Native Method Stacks)

和虛擬棧相似,只不過它服務於Native方法線程私有。當 Java 虛擬機使用其他語言(例如 C 語言)來實現指令集解釋器時,也會使用到本地方法棧。如果 Java 虛擬機不支持 natvie 方法,並且自己也不依賴傳統棧的話,可以無需支持本地方法棧。

與 Java 虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowErrorOutOfMemoryError 異常。

HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。

4.3、程序計數器

當前線程所執行的字節碼的行號指示器,用於記錄正在執行的虛擬機字節指令地址,線程私有。

需要特別注意的是,程序計數器是唯一一個在Java虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域。

5、JVM 內存結構總結

在這里插入圖片描述
程序計數器:

1、 當前線程所執行的字節碼的行號指示器,用於記錄正在執行的虛擬機字節指令地址,線程私有。
2、程序計數器是唯一一個在Java虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域。

Java虛擬棧:

1、存放基本數據類型、對象的引用、方法出口等,線程私有。
2、棧容量超過 Java 虛擬機棧的最大容量,會拋出 StackOverflowError 異常;也就是棧溢出錯誤!方法遞歸產生
3、如果 Java 虛擬機棧可以動態擴展,無法申請到足夠的內存或者在創建新的線程時沒有足夠的內存去創建對應的 Java 虛擬機棧,會拋出 OutOfMemoryError 異常。也就是OOM內存溢出錯誤!(線程啟動過多)
4、參數 -Xss 調整JVM棧的大小

Native方法棧:

1、和虛擬棧相似,只不過它服務於Native方法,線程私有。
2、HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。

Java堆:

java內存最大的一塊,所有對象實例、數組都存放在java堆,GC回收的地方,線程共享。

 

Java堆 = 老年代 + 新生代

 

新生代 = Eden + S0 + S1

 

默認Eden:from :to = 8:1:1

方法區:

1、存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼數據等,回收目標主要是常量池的回收和類型的卸載,各線程共享
2、方法區JDK1.7的時候叫做永久代,到JDK1.8之后廢棄了永久代改為元空間(meta space)

如果本文對你有一點點幫助,那么請點個贊唄,謝謝~

最后,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,向往技術,追求技術,說好了來了就是盆友喔...

在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM