天天看點

Java記憶體管理:Java記憶體區域 JVM運作時資料區

Java記憶體管理:Java記憶體區域 JVM運作時資料區

       在前面的一些文章了解到javac編譯的大體過程、Class檔案結構、以及JVM位元組碼指令。
       下面我們詳細了解Java記憶體區域:先說明JVM規範定義的JVM運作時配置設定的資料區有哪些,然後分别介紹它們的特點,并指出給出一些HotSpot虛拟機實作的不同點和調整參數。

1、Java記憶體區域概述

1-2、C/C++與Java程式開發的記憶體管理

       在記憶體管理領域,C/C++程式開發與Java程式開發有着完全不同的理念:

1、C/C++程式開發

       自己管理記憶體是一項基礎的工作;

       自已配置設定記憶體,但也得自己來及時回收;

       比較自由,但多了些工作量,且容易出現記憶體洩露和記憶體溢出等問題;

2、Java程式開發

       JVM管理記憶體,不需要自己手動配置設定記憶體和釋放記憶體;

       不容易出現記憶體洩露和記憶體溢出;

       一旦出現問題不容易排查,是以得了解JVM是怎麼使用記憶體;

1-2、Java記憶體區域與JVM運作時資料區

Java記憶體管理:Java記憶體區域 JVM運作時資料區
       如上圖, Java虛拟機規範定義了位元組碼執行期間使用的各種運作時資料區,即JVM在執行Java程式的過程中,會把它管理的記憶體劃分為若幹個不同的資料區域,包括:
      程式計數器、java虛拟機棧、本地方法棧、java堆、方法區、運作時常量池;

       從線程共享角度來說,可以分為兩類:

1、所有線程共享的資料區

       方法區、運作時常量池、java堆;

       這些資料區域是在Java虛拟機啟動時建立的,隻有當Java虛拟機退出時才會被銷毀;

2、線程間隔離的資料區

       程式計數器、java虛拟機棧、本地方法棧、

       這些資料區域是每個線程的"私有"資料區,每個線程都有自己的,不與其他線程共享;

       每個線程的資料區在建立線程時建立,并線上程退出時被銷毀;

3、另外,還一種特殊的資料區

      直接記憶體--使用Native函數庫直接配置設定的堆外記憶體;

       即Java記憶體區域 = JVM運作時資料區 +直接記憶體。

2、Java各記憶體區域說明

       上面圖檔展示的是JVM規範定義的運作時資料概念模型,實際上JVM的實作可能有所差别,下面在介紹各記憶體資料區時會給出一些HotSpot虛拟機實作的不同點和調整參數。

2-1、程式計數器

       程式計數器(Program Counter Register),簡稱PC計數器;

1、生存特點

       每個線程都需要一個獨立的PC計數器,生命周期與所屬線程相同,各線程的計數器互不影響;

2、作用

      JVM位元組碼解釋器通過改變這個計數器的值來選取線程的下一條執行指令;

3、存儲内容

       JVM的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作的;

       在任意時刻,一個線程隻會執行一個方法的代碼(稱為該線程的目前方法(Current Method));        

(A)、如果這個方法是Java方法,那PC計數器就儲存JVM正在執行的位元組碼指令的位址;

(B)、如果該方法是native的,那PC計數器的值是空(undefined);

4、記憶體配置設定特點

       PC計數器占用較小的記憶體空間;

       容量至少應當能儲存一個returnAddress類型的資料或者一個與平台相關的本地指針的值;

5、異常情況

       唯一一個JVM規範中沒有規定會抛出OutOfMemoryError情況的區域;

2-2、Java虛拟機棧

       Java虛拟機棧(Java Virtual Machine Stack,JVM Stack),指常說的棧記憶體(Stack);

       和Java堆指的堆記憶體(Heap),都是需要重點關注的記憶體區域;

1、生存特點

       每個線程都有一個私有的,生命周期與所屬線程相同;

2、作用

       描述的是Java方法執行的記憶體模型,與傳統語言中(如C/C++)的棧類似;

       在方法調用和傳回中也扮演了很重要的角色;

3、存儲内容

       用于儲存方法的棧幀(Stack Frame);

       每個方法從調用到執行結束,對應其棧幀在JVM棧上的入棧到出棧的過程;

           棧幀:

       每個方法執行時都會建立一個棧幀,随着方法調用而建立(入棧),随着方法結束而銷毀(出棧);

       棧幀是方法運作時的基礎結構;

       棧幀用于存儲局部變量表、操作數棧、動态連接配接、方法出口等資訊;

(A)、局部變量表

       局部變量表(Local Variables Table)是一組變量值存儲空間,用于存放方法參數和方法内部定義的局部變量。

       這些都是在編譯期可知的資料,是以一個方法調用時,在JVM棧中配置設定給該方法的局部變量空間是完全确定的,運作中不改變;

       一個方法配置設定局部變量表的最大容量由Class檔案中該方法的Code屬性的max_locals資料項确定;

(B)、操作數棧

       操作數棧(Operand Stack)簡稱操作棧,它是一個後進先出(Last-In-First-Out,LIFO)棧;

       在方法的執行過程中,會有各種位元組碼指令往操作數棧中寫入和提取内容(任意類型的值),也就是入棧/出棧操作;

       在方法調用的時候,操作數棧也用來準備調用方法的參數以及接收方法傳回結果;

       一個方法的操作數棧長度由Class檔案中該方法的Code屬性的max_stacks資料項确定;

(C)、動态連結

       每一個棧幀内部都包含一個指向運作時常量池的引用,來支援目前方法的執行過程中實作動态連結 (Dynamic Linking);

       在 Class 檔案裡面,描述一個方法調用了其他方法,或者通路其成員變量是通過符号引用(Symbolic Reference)來表示的;

      動态連結的作用就是将這些符号引用所表示的方法轉換為實際方法的直接引用(除了在類加載階段解析的一部分符号);

4、記憶體配置設定特點

       因為除了棧幀的出棧和入棧之外,JVM棧從來不被直接操作,是以棧幀可以在堆中配置設定;

       JVM棧所使用的記憶體不需要保證是連續的;

       JVM規範允許JVM棧被實作成固定大小的或者是根據計算動态擴充和收縮的:

(A)、固定大小

       如果JVM棧是固定大小的,則當建立新線程的棧時,可以獨立地選擇每個JVM棧的大小;

(B)、動态擴充或收縮

       在動态擴充或收縮JVM棧的情況下,JVM實作應該提供調節JVM棧最大和最小記憶體空間的手段;

兩種情況下,JVM實作都應當提供調節JVM棧初始記憶體空間大小的手段;

      HotSpot VM通過"-Xss"參數設定JVM棧記憶體空間大小;

5、異常情況

       JVM規範中對該區域,規定了兩種可能的異常狀況:

(A)、StackOverflowError

       如果線程請求配置設定的棧深度超過JVM棧允許的最大深度時,JVM将會抛出一個StackOverflowError異常;

(B)、 OutOfMemoryError

       如果JVM棧可以動态擴充,當然擴充的動作目前無法申請到足夠的記憶體去完成擴充,或者在建立新的線程時沒有足夠的記憶體去建立對應的虛拟機棧,那JVM将會抛出一個OutOfMemoryError異常;

該區域與方法執行的JVM位元組碼指令密切相關,這裡篇幅有限,以後有時間會分析方法的調用與執行過程,再來詳細介紹該區域。

2-3、本地方法棧

       本地方法棧(Native Method Stack)與 Java虛拟機棧類似;

 1、與Java虛拟機棧的差別

       Java虛拟機棧為JVM執行Java方法(也就是位元組碼)服務;

       本地方法棧則為Native方法(指使用Java以外的其他語言編寫的方法)服務;

2、HotSpot VM實作方式

       JVM規範中沒有規定本地方法棧中方法使用的語言、方式和資料結構,JVM可以自由實作;

      HotSpot VM直接把本地方法棧和Java虛拟機棧合并為一個;

2-4、Java堆

       Java堆(Java Heap)指常說的堆記憶體(Heap);

1、生存特點

       所有線程共享;

       生命周期與JVM相同;

2、作用

      為"new"建立的執行個體對象提供存儲空間;

       裡面存儲的這些對象執行個體都是通過垃圾收集器(Garbage Collector)進行自動管理,是以Java堆也稱"GC堆"(Garbage Collected Heap);

對GC堆以及GC的參數設定調整,就是JVM調優的主要内容;

3、存儲内容

      用于存放幾乎所有對象執行個體;

       (随JIT編譯技術和逃逸分析技術發展,少量對象執行個體可能在棧上配置設定,詳見後面介紹JIT編譯的文章);

4、記憶體配置設定特點

(A)、Java堆劃分

       為更好回收記憶體,或更快配置設定記憶體,需要對Java堆進行劃分:

(I)、從垃圾收集器的角度來看

       JVM規範沒有規定JVM如何實作垃圾收集器;

       由于很多JVM采用分代收集算法,是以Java堆還可以細分為:新生代、老年代和永久代;

(II)、從記憶體配置設定角度來看

       為解決配置設定記憶體線程不安全問題,需要同步處理;

       Java堆可能劃分出每個線程私有的配置設定緩沖區(Thread Local Allocation Buffer,TLAB),減少線程同步;

HotSpot VM通過"-XX:+/-UseTLAB"指定是否使用TLAB;

(B)、配置設定調整

       和JVM棧一樣,Java堆所使用的實體記憶體不需要保證是連續的,邏輯連續即可;

       JVM規範允許Java堆被實作成固定大小的或者是根據計算動态擴充和收縮的:

       兩種情況下,JVM實作都應當提供調節JJava堆初始記憶體空間大小的手段;

       在動态擴充或收縮的情況下,還應該提供調節最大和最小記憶體空間的手段;

(C)、HotSpot VM相關調整

       目前主流的JVM都把Java堆實作成動态擴充的,如HotSpot VM:

(1)、初始空間大小

       通過"-Xms"或"-XX:InitialHeapSize"參數指定Java堆初始空間大小;

       預設為1/64的實體記憶體空間;

(2)、最大空間大小

       通過"-Xmx"或"-XX:MaxHeapSize"參數指定ava堆記憶體配置設定池的最大空間大小;

       預設為1/4的實體記憶體空間;

       Parallel垃圾收集器預設的最大堆大小是當小于等于192MB實體記憶體時,為實體記憶體的一半,否則為實體記憶體的四分之一;

(3)、各年代記憶體的占用空間與可用空間的比例

       通過"-XX:MinHeapFreeRatio"和"-XX:MaxHeapFreeRatio"參數設定堆中各年代記憶體的占用空間與可用空間的比例保持在特定範圍内;

       預設:

       "-XX:MinHeapFreeRatio=40":即一個年代(新生代或老年代)記憶體空餘小于40%時,JVM會從未配置設定的堆記憶體中配置設定給該年代,以保持該年代40%的空餘記憶體,直到配置設定完"-Xmx"指定的堆記憶體最大限制;
       "-XX:MaxHeapFreeRatio=70":即一個年代(新生代或老年代)記憶體空餘大于70%時,JVM會縮減該年代記憶體,以保持該年代70%的空餘記憶體,直到縮減到"-Xms"指定的堆記憶體最小限制;

      這兩個參數不适用于Parallel垃圾收集器(通過“-XX:YoungGenerationSizeIncrement”、“-XX:TenuredGenerationSizeIncrement ”能及“-XX:AdaptiveSizeDecrementScaleFactor”調節);

(4)、年輕代與老年代的大小比例

       通過"-XX:NewRatio":控制年輕代與老年代的大小比例;

       預設設定"-XX:NewRatio=2"表新生代和老年代之間的比例為1:2;

       換句話說,eden和survivor空間組合的年輕代大小将是總堆大小的三分之一;

(5)、年輕代空間大小

       通過"-Xmn"參數指定年輕代(nursery)的堆的初始和最大大小;

       或通過"-XX:NewSize"和"-XX:MaxNewSize"限制年輕代的最小大小和最大大小;

(6)、定永久代空間大小

       通過"-XX:MaxPermSize(JDK7)"或"-XX:MaxMetaspaceSize(JDK8)"參數指定永久代的最大記憶體大小;

       通過"-XX:PermSize(JDK7)"或"-XX:MetaspaceSize(JDK8)"參數指定永久代的記憶體門檻值--超過将觸發垃圾回收;

       注:JDK8中永久代已被删除,類中繼資料存儲空間在本地記憶體中配置設定;

       詳情請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62

(D)、調整政策

      關于這些參數的調整需要垃圾收集的一些知識(以後文章會介紹),先來簡單了解:

      當使用某種并行垃圾收集器時,應該指定期望的具體行為而不是指定堆的大小;

      讓垃圾收集器自動地、動态的調整堆的大小來滿足期望的行為;

      調整的一般規則:

      除非你的應用程式無法接受長時間的暫停,否則你可以将堆調的盡可能大一些;

      除非你發現問題的原因在于老年代的垃圾收集或應用程式暫停次數過多,否則你應該将堆的較大部分分給年輕代;

關于HotSpot虛拟機堆記憶體分代說明以及空間大小說明請參考:

http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html#sthref16

http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html#sizing_generations

5、異常情況

      如果實際所需的堆超過了垃圾收集器能提供的最大容量,那Java虛拟機将會抛出一個OutOfMemoryError異常;

該部分的記憶體如何配置設定、垃圾如何收集,上面這些參數如何調整,将在以後的文章詳細說明。

2-5、方法區

      方法區(Method Area)是堆的邏輯組成部分,但有一個别名"Non-Heap"(非堆)用以區分;

1、生存特點

      所有線程共享;

      生命周期與JVM相同;

2、作用

      為類加載器加載Class檔案并解析後的類結構資訊提供存儲空間;

      以及提供JVM運作時常量存儲的空間;

3、存儲内容

      用于存儲JVM加載的每一個類的結構資訊,主要包括:

(A)、運作時常量池(Runtime Constant Pool)、字段和方法資料;

(B)、構造函數、普通方法的位元組碼内容以及JIT編譯後的代碼;

(C)、還包括一些在類、執行個體、接口初始化時用到的特殊方法;

4、記憶體配置設定特點

(A)、配置設定調整

      和Java堆一樣,所使用的實體記憶體不需要保證是連續的;

      或以實作成固定大小的或者是根據計算動态擴充和收縮的;

(B)、方法區的實作與垃圾回收

      JVM規範規定:

      雖然方法區是堆的邏輯組成部分,但不限定實作方法區的記憶體位置;

      甚至簡單的虛拟機實作可以選擇在這個區域不實作垃圾收集;

      因為垃圾收集主要針對常量池和類型解除安裝,效果不佳;

      但方法區實作垃圾回收是必要的,否則容易引起記憶體溢出問題;

(C)、HotSpot VM相關調整

(I)、在JDK7中

      使用永久代(Permanent Generation)實作方法區,這樣就可以不用專門實作方法區的記憶體管理,但這容易引起記憶體溢出問題;

      有規劃放棄永久代而改用Native Memory來實作方法區;

      不再在Java堆的永久代中生成中配置設定字元串常量池,而是在Java堆其他的主要部分(年輕代和老年代)中配置設定;

      更多請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html

(II)、在JDK8中

      永久代已被删除,類中繼資料(Class Metadata)存儲空間在本地記憶體中配置設定,并用顯式管理中繼資料的空間:

      從OS請求空間,然後分成塊;

      類加載器從它的塊中配置設定中繼資料的空間(一個塊被綁定到一個特定的類加載器);

      當為類加載器解除安裝類時,它的塊被回收再使用或傳回到作業系統;

      中繼資料使用由mmap配置設定的空間,而不是由malloc配置設定的空間;

      通過"-XX:MaxMetaspaceSize" (JDK8)參數指定類中繼資料區的最大記憶體大小;

      通過"-XX:MetaspaceSize" (JDK8)參數指定類中繼資料區的記憶體門檻值--超過将觸發垃圾回收;

      詳情請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62

5、異常情況

      如果方法區的記憶體空間不能滿足記憶體配置設定請求,那Java虛拟機将抛出一個OutOfMemoryError異常;

2-6、運作常量池

      運作常量池(Runtime Constant Pool)是方法區的一部分;

1、存儲内容

      是每一個類或接口的常量池(Constant_Pool)的運作時表示形式;

      包括了若幹種不同的常量:

      (A)、從編譯期可知的字面量和符号引用,也即Class檔案結構中的常量池;

      (B)、必須運作期解析後才能獲得的方法或字段的直接引用;

      (C)、還包括運作時可能建立的新常量(如JDK1.6中的String類intern()方法)

2-7、直接記憶體

      直接記憶體(Direct Memory)不是JVM運作時資料區,也不是JVM規範中定義的記憶體區域;

1、特點

      是使用Native函數庫直接配置設定的堆外記憶體;

      被頻繁使用,且容易出現OutOfMemoryError異常;

2、作用

      因為避免了在Java堆中來回複制資料,能在一些場景中顯著提高性能;

3、實作方式

      JDK1.4中新加入NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式;

      它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java椎中的DirectByteBuffer對象作為這塊記憶體的引用進行操作;

4、HotSpot VM相關調整

      可以通過"-XX:MaxDirectMemorySize"參數指定直接記憶體最大空間;

      不會受到Java堆大小的限制,即"-Xmx"參數限制的空間不包括直接記憶體;

      這容易導緻各個記憶體區域總和大于實體記憶體限制,出現OutOfMemoryError異常;

      到這裡,我們大體了解Java各記憶體區域是什麼,有些什麼特點了,但方法執行的JVM位元組碼指令如何在Java虛拟棧中運作的,以及Java堆記憶體如何配置設定、垃圾如何收集,如何進行JVM調優,将在以後的文章詳細說明。

      後面我們将分别去了解:方法的調用與執行、JIT編譯--在運作時把Class檔案位元組碼編譯成本地機器碼的過程、以及JVM垃圾收集相關内容……

【參考資料】

1、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

2、《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html

3、《Memory Management in the Java HotSpot™ Virtual Machine》:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

4、HotSpot虛拟機參數官方說明:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

5、《深入了解Java虛拟機:JVM進階特性與最佳實踐》第二版 第2章

6、Java Class檔案結構解析 及 執行個體分析驗證