1. 程式人生 > >JVM內存堆布局圖解分析

JVM內存堆布局圖解分析

new 移動 art 文檔 rman 單個 線程 改變 完成

轉載原文出處:http://www.codeceo.com/article/jvm-memory-stack.html

JAVA能夠實現跨平臺的一個根本原因,是定義了class文件的格式標準,凡是實現該標準的JVM都能夠加載並解釋該class文件,據此也可以知道,為啥Java語言的執行速度比C/C++語言執行的速度要慢了,當然原因肯定不止這一個,如在JVM中沒有數據寄存器,指令集使用的是棧來保存中間數據…等,盡管Java的貢獻者們為執行速度的提高想了各種辦法,如JIT、動態編譯器等,以下是Leetcode中一道題目用不同的語言實現時的執行性能對比圖…

以下是JVM的一個基本架構圖,在這個基本架構圖中,棧有兩部份,Java線程棧以及本地方法棧,棧的概念與C/C++程序基本上都是一個概念,裏面存放的都是棧幀,一個棧幀代表的就是一個函數的調用,在棧幀裏面存放了函數的形參,函數的局部變量, 返回地址等,但是與C/C++的一個重要區別是,C/C++裏面有傳值以及傳址的區別,當傳的是一個對象時( 結構體也可以當成對象,其實就是對象~,只不過裏面的方法默認都是public的,不信你可以試試,在結構體中加一個函數,編譯器也不會報錯,程序依舊運行~~~),會將對象復到到棧中,而Java中只有基本類型才是傳值的,其他類型傳的都是引用,什麽是引用,學過C/C++的就把引用當作指針理解吧~~~,在這個基本架構圖中,可以看出JVM還定義了一個本地方法棧,本地方法棧是為Java調用本地方法【這些本地方法是由其他語言編寫的】服務的

上面的圖中看到的是JVM中棧有兩個,但是堆只有一個,每一個線程都有自已的線程棧【線程棧的大小可以通過設置JVM的-xss參數進行配置,32位系統下,JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K】,線程棧裏面的數據屬於該線程私有,但是所有的線程都共享一個堆空間,堆中存放的是對象數據,什麽是對象數據,排除法,排除基本類型以及引用類型以外的數據都將放在堆空間中。其中方法區和堆是所有線程共享的數據區。

技術分享

1.程序計數器

  在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這裏,虛擬機不使用CPU的程序計數器,自己在內存中設立一片區域來模擬CPU的程序計數器。只有一個程序計數器是不夠的,當多個線程切換執行時,那就單個程序計數器就沒辦法了,虛擬機規範中指出,每一條線程都有一個獨立的程序計數器。註意,Java虛擬機中的程序計數器指向正在執行的字節碼地址,而不是下一條。

2. Java虛擬機棧

  Java虛擬機棧也是線程私有的,虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的時候都會創建一個棧幀,用於存放局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法從調用直到執行完成的過程都對應著一個棧幀在虛擬機中的入棧到出棧的過程。我們平時把內存分為堆內存和棧內存,其中的棧內存就指的是虛擬機棧的局部變量表部分。局部變量表存放了編譯期可以知道的基本數據類型(boolean、byte、char、short、int、float、long、double),對象引用(可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置),和返回後所指向的字節碼的地址。其中64 位長度的long 和double 類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。當遞歸層次太深時,會引發java.lang.StackOverflowError,這是虛擬機棧拋出的異常。

3. 本地方法棧

  在HotSpot虛擬機將本地方法棧和虛擬機棧合二為一,它們的區別在於,虛擬機棧為執行Java方法服務,而本地方法棧則為虛擬機使用到的Native方法服務。

4. Java堆

  Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。這個區域是用來存放對象實例的,幾乎所有對象實例都會在這裏分配內存。堆是Java垃圾收集器管理的主要區域(GC堆),垃圾收集器實現了對象的自動銷毀。Java堆可以細分為:新生代和老年代;再細致一點的有Eden空間,From Survivor空間,To Survivor空間等。Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。可以通過-Xmx和-Xms控制

5. 方法區

  方法區也叫永久代。在過去(自定義類加載器還不是很常見的時候),類大多是”static”的,很少被卸載或收集,因此被稱為“永久的(Permanent)”。雖然Java 虛擬機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區分開來。同時,由於類class是JVM實現的一部分,並不是由應用創建的,所以又被認為是“非堆(non-heap)”內存。HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。

  永久代也是各個線程共享的區域,它用於存儲已經被虛擬機加載過的類信息,常量,靜態變量(JDK7中被移到Java堆),即時編譯期編譯後的代碼(類方法)等數據。這裏要講一下運行時常量池,它是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用(其實就是八大基本類型的包裝類型和String類型數據(JDK7中被移到Java堆))(官方文檔說明: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application)。

  在JDK1.7中的HotASpot中,已經把原本放在方法區的字符串常量池移出。

  • 將interned String移到Java堆中
  • 將符號Symbols移到native memory(不受GC管理的內存)

  從JDK7開始永久代的移除工作,貯存在永久代的一部分數據已經轉移到了Java Heap或者是Native Heap。但永久代仍然存在於JDK7,並沒有完全的移除:符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。隨著JDK8的到來,JVM不再有PermGen。但類的元數據信息(metadata)還在,只不過不再是存儲在連續的堆空間上,而是移動到叫做“Metaspace”的本地內存(Native memory)中。

在JVM中共享數據空間劃分如下圖所示

技術分享

上圖中,刻畫了Java程序運行時的堆空間,可以簡述成如下2條

1.JVM中共享數據空間可以分成三個大區,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分為新生代和老年代

2.新生代可以劃分為三個區,Eden區(存放新生對象),兩個幸存區(From Survivor和To Survivor)(存放每次垃圾回收後存活的對象)

3.永久代管理class文件、靜態對象、屬性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )

4.JVM垃圾回收機制采用“分代收集”:新生代采用復制算法,老年代采用標記清理算法。

作為操作系統進程,Java 運行時面臨著與其他進程完全相同的內存限制:操作系統架構提供的可尋址地址空間和用戶空間。

操 作系統架構提供的可尋址地址空間,由處理器的位數決定,32 位提供了 2^32 的可尋址範圍,也就是 4,294,967,296 位,或者說 4GB。而 64 位處理器的可尋址範圍明顯增大:2^64,也就是 18,446,744,073,709,551,616,或者說 16 exabyte(百億億字節)。

地址空間被劃分為用戶空間和內核空間。內核是主要的操作系統程序和C運行時,包含用於連接計算機硬件、調度程序以及提供聯網和虛擬內存等服務的邏輯和基於C的進程(JVM)。除去內核空間就是用戶空間,用戶空間才是 Java 進程實際運行時使用的內存

默認情況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內核空間。在一些 Windows 版本上,通過向啟動配置添加 /3GB 開關並使用 /LARGEADDRESSAWARE 開關重新鏈接應用程序,可以將這種平衡調整為 3GB 用戶空間和 1GB 內核空間。在 32 位 Linux 上,默認設置為 3GB 用戶空間和 1GB 內核空間。一些 Linux 分發版提供了一個hugemem內核,支持 4GB 用戶空間。為了實現這種配置,將進行系統調用時使用的地址空間分配給內核。通過這種方式增加用戶空間會減慢系統調用,因為每次進行系統調用時,操作系統必須在地址空間之間復制數據並重置進程地址-空間映射。

下圖為一個32 位 Java 進程的內存布局:

技術分享

可尋址的地址空間總共有 4GB,OS 和 C 運行時大約占用了其中的 1GB,Java 堆占用了將近 2GB,本機堆占用了其他部分。請註意,JVM 本身也要占用內存,就像 OS 內核和 C 運行時一樣。

註意:

1. 上文提到的可尋址空間即指最大地址空間。

2. 對於2GB的用戶空間,理論上Java堆內存最大為1.75G,但一旦Java線程的堆達到1.75G,那麽就會出現本地堆的Out-Of-Memory錯誤,所以實際上Java堆的最大可使用內存為1.5G。

在JVM運行時,可以通過配置以下參數改變整個JVM堆的配置比例

1.Java heap的大小(新生代+老年代)

  -Xms堆的最小值

  -Xmx堆空間的最大值

2.新生代堆空間大小調整

  -XX:NewSize新生代的最小值

  -XX:MaxNewSize新生代的最大值

  -XX:NewRatio設置新生代與老年代在堆空間的大小

  -XX:SurvivorRatio新生代中Eden所占區域的大小

3.永久代大小調整

  -XX:MaxPermSize

4.其他

   -XX:MaxTenuringThreshold,設置將新生代對象轉到老年代時需要經過多少次垃圾回收,但是仍然沒有被回收

在上面的配置中,老年代所占空間的大小是由-XX:SurvivorRatio這個參數進行配置的,看完了上面的JVM堆空間分配圖,可能會奇怪,為啥新生代空間要劃分為三個區Eden及兩個Survivor區?有何用意?為什麽要這麽分?要理解這個問題,就得理解一下JVM的垃圾收集機制(復制算法也叫copy算法),步驟如下:

復制(Copying)算法

將內存平均分成A、B兩塊,算法過程:

1. 新生對象被分配到A塊中未使用的內存當中。當A塊的內存用完了, 把A塊的存活對象對象復制到B塊。
2. 清理A塊所有對象。
3. 新生對象被分配的B塊中未使用的內存當中。當B塊的內存用完了, 把B塊的存活對象對象復制到A塊。
4. 清理B塊所有對象。
5. goto 1。

優點:簡單高效。缺點:內存代價高,有效內存為占用內存的一半。

圖解說明如下所示:(圖中後觀是一個循環過程)

對復制算法進一步優化:使用Eden/S0/S1三個分區

平均分成A/B塊太浪費內存,采用Eden/S0/S1三個區更合理,空間比例為Eden:S0:S1==8:1:1,有效內存(即可分配新生對象的內存)是總內存的9/10。

算法過程:

1. Eden+S0可分配新生對象;
2. 對Eden+S0進行垃圾收集,存活對象復制到S1。清理Eden+S0。一次新生代GC結束。
3. Eden+S1可分配新生對象;
4. 對Eden+S1進行垃圾收集,存活對象復制到S0。清理Eden+S1。二次新生代GC結束。
5. goto 1。

默認Eden:S0:S1=8:1:1,因此,新生代中可以使用的內存空間大小占用新生代的9/10,那麽有人就會問,為什麽不直接分成兩個區,一個區占9/10,另一個區占1/10,這樣做的原因大概有以下幾種

1.S0與S1的區間明顯較小,有效新生代空間為Eden+S0/S1,因此有效空間就大,增加了內存使用率
2.有利於對象代的計算,當一個對象在S0/S1中達到設置的XX:MaxTenuringThreshold值後,會將其分到老年代中,設想一下,如果沒有S0/S1,直接分成兩個區,該如何計算對象經過了多少次GC還沒被釋放,你可能會說,在對象裏加一個計數器記錄經過的GC次數,或者存在一張映射表記錄對象和GC次數的關系,是的,可以,但是這樣的話,會掃描整個新生代中的對象, 有了S0/S1我們就可以只掃描S0/S1區了~~~

JVM內存堆布局圖解分析