java筆記(JVM Memory Structure)

本文摘自:《深刻理解Java虛擬機》的第2章內容java

Image

java運行時數據區域,主要包括了方法區(Method Area),java棧區(java stack),本地方法棧區(native method),堆(heap)和程序計數器(program counter register)程序員

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

程序計數器(Program Counter Register)是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。數組

因爲Java虛擬機的多線是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。 若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。服務器

2.Java虛擬機棧(Java Virtual Machine Stacks)數據結構

與程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口信息。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。函數

常常有人把Java內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域是這兩塊。其中所指的「堆」在後面會專門講述,而所指的「棧」就是如今講的虛擬機棧,或者說是虛擬機棧中的局部變量表部分。性能

局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型),它不等同於對象自己,根據不一樣的虛擬機實現,它多是一個指向對象起始地址的引用指針,也可能指向一個表明對象的句柄或者其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。測試

其中64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其他的數據類型只佔用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。 在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError異常。優化

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

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

4.Java堆

對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在Java虛擬機規範中的描述是:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼「絕對」了。

Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」(Garbage Collected Heap,幸虧國內沒翻譯成「垃圾堆」)。若是從內存回收的角度看,因爲如今收集器基本都是採用的分代收集算法,因此Java堆中還能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。若是從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過,不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。在本章中,咱們僅僅針對內存區域的做用進行討論,Java堆中的上述各個區域的分配和回收等細節將會是下一章的主題。

根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

5.方法區(Method Area)

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

對於習慣在HotSpot虛擬機上開發和部署程序的開發者來講,不少人願意把方法區稱爲「永久代」(Permanent Generation),本質上二者並不等價,僅僅是由於HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其餘虛擬機(如BEA JRockit、IBM J9等)來講是不存在永久代的概念的。即便是HotSpot虛擬機自己,根據官方發佈的路線圖信息,如今也有放棄永久代並「搬家」至Native Memory來實現方法區的規劃了。

Java虛擬機規範對這個區域的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,通常來講這個區域的回收「成績」比較難以使人滿意,尤爲是類型的卸載,條件至關苛刻,可是這部分區域的回收確實是有必要的。在Sun公司的BUG列表中,曾出現過的若干個嚴重的BUG就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存泄漏。 根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

6.運行時常量池(Runtime Constant Pool)

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。Java虛擬機對Class文件的每一部分(天然也包括常量池)的格式都有嚴格的規定,每個字節用於存儲哪一種數據都必須符合規範上的要求,這樣纔會被虛擬機承認、裝載和執行。但對於運行時常量池,Java虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。不過,通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。 運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性Java語言並不要求常量必定只能在編譯期產生也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是String類的intern()方法。 既然運行時常量池是方法區的一部分,天然會受到方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。

7.本機直接內存(Direct Memory)

直接內存並非虛擬機運行時數據區的一部分,它根本就是本機內存而不是VM直接管理的區域。可是這部份內存也會致使OutOfMemoryError異常出現,所以咱們放到這裏一塊兒描述。在JDK1.4中新加入了NIO類,引入一種基於渠道與緩衝區的I/O方式,它能夠經過本機Native函數庫直接分配本機內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯着提升性能,由於避免了在Java對和本機堆中來回複製數據。顯然本機直接內存的分配不會受到Java堆大小的限制,可是即然是內存那確定仍是要受到本機物理內存(包括SWAP區或者Windows虛擬內存)的限制的,通常服務器管理員配置JVM參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),而致使動態擴展時出現OutOfMemoryError異常。


Java堆溢出

import java.util.ArrayList;

import java.util.List;

/**

* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

*/

public class HeapOOM {

static class OOMObject {

     }

public static void main(String[] args) {

           List<OOMObject> list = new ArrayList<OOMObject>();

while ( true) {

                list.add( new OOMObject());

           }

     }

}

[GC 6144K->4109K(20480K), 0.0288625 secs]

[GC 8804K->8500K(20480K), 0.0160519 secs]

[Full GC 17810K->13307K(20480K), 0.3890937 secs]

[Full GC 16378K->16308K(20480K), 0.2906083 secs]

[Full GC 16308K->16297K(20480K), 0.3850525 secs]

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid6012.hprof ...

Heap dump file created [27878579 bytes in 0.229 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space


虛擬機棧和本地方法棧溢出

  • 若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverFlowError
  • 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError

虛擬機棧和本地方法棧OOM測試


/**

* VM Args : -Xss128k

*/

public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak() {

stackLength++;

           stackLeak();

     }

public static void main(String[] args) {

           JavaVMStackSOF oom = new JavaVMStackSOF();

try {

                oom.stackLeak();

           } catch (Throwable e) {

                System. out.println( "stack length:" + oom.stackLength );

throw e;

           }

     }

}

stack length:11424

Exception in thread "main" java.lang.StackOverflowError

     at zt.OOM.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14 )

     at zt.OOM.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15 )

     at zt.OOM.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15 )


運行時常量池致使的內存溢出異常

import java.util.ArrayList;

import java.util.List;

/**

* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M

*

*/

public class RuntimeConstantPoolOOM {

public static void main(String[] args) {

// 使用List保持着常量池引用,避免Full GC回收常量池行爲

           List<String> list = new ArrayList<String>();

// 10MB的PermSize在integer範圍內足夠產生OOM了

int i = 0;

while ( true) {

                list.add(String. valueOf(i++).intern());

           }

     }

}



使用unsafe分配本機內存

import java.lang.reflect.Field;

import sun.misc.Unsafe;

/**

* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M

*

*/

public class DirectMemoryOOM {

private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {

           Field unsafeField = Unsafe.class .getDeclaredFields()[0];

           unsafeField.setAccessible( true);

           Unsafe unsafe = (Unsafe) unsafeField.get( null);

while ( true) {

                unsafe.allocateMemory( _1MB);

           }

     }

}

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

     at java.lang.Integer.toString( Integer.java:331 )

     at java.lang.String.valueOf( String.java:2959 )

     at zt.OOM.test.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:21 )

由DirectMemory致使的內存溢出,一個明顯的特徵是在Heap Dump文件中不會看見明顯的異常,若是讀者發現OOM以後Dump文件很小,二程序中又直接或間接使用了NIO,那就能夠考慮檢查一下是否是這方面的緣由。