Java Memory Model--學習小結

爲什麼要學習Java Memory Model?

單從工具的使用上來說,Java內存模型對於實際的代碼編寫可能不會產生直接的影響。因爲Java在很大程度上已經屏蔽了程序員與內存的直接關聯。通過Java虛擬機(JVM),程序員可以安全的創建、使用內存數據單元,JVM也會自動的對內存進行垃圾回收處理(Garbage Collector)。但對於一個致力於或者以此爲生的工作者來說,不瞭解JMM,不瞭解Java內存的分配,就意味着你可能根本就不清楚以下這些:

1. 爲什麼說早期的Java性能會不如C/C++?

2. 如何提高Java的性能?

3. Java的內存分配、訪問和管理比C++具有什麼優勢?

4. (3)的優勢是如何實現的?

5. Java的動態加載特性如何實現?

6. 有哪些情況會出現Out of Memory(OOM)?

7. 出現OOM該如何確定錯誤原因?

……

如果只是漫無目的的因爲使用而使用一門語言,那麼其實Python,C,C++,C#,.Net,Perl……任何一門語言都可以滿足你的需求,既然已經致力於此,又爲何不開始一段新的旅程呢?

自勉並以此作爲共勉。JMM,可以作爲學習Java的第一步……


字典

縮寫 全稱
JMM Java Memory Model
JVM Java Virtual Mechine
OMM OutOfMemory
PC Program counter
GC Garbage Collector

1. Java Virtual Memory對運行時內存區域的劃分

JVM將其管理的運行時內存區域大致劃分爲兩個區域:1)線程隔離數據區;2)線程共享數據區。根據《Java虛擬機規範規定》,可以將其劃分爲這麼幾個部分:

下面就運行時數據區模型的各個組成部分進行介紹。


2. 線程隔離數據區--程序計數器(Program Counter Register)

程序計數器作爲當前線程執行字節碼的“行號指示器”,其在內存區域的分配區域固定,且佔用空間極小。程序計數器能夠幫助字節碼解釋器選取下一條需要執行的字節碼指令。在多線程切換時,也需要依賴程序計數器進行線程恢復工作。因此,爲了避免各個線程之間的相互影響,程序計數器必須作爲“線程私有”。PC沒有指定Out Of Memory的任何情況(如果有,那可能是因爲平臺字節碼指令地址超過PC的指定寬度,不過這幾乎是不可能的)


3. 線程隔離數據區--虛擬機棧(Java Virtual Mechine Stacks)

虛擬機棧、本地方法棧和方法區,是我在學習過程中碰到容易混淆的幾個概念點。先從虛擬機棧開始說起。

虛擬機棧是Java方法執行的內存模型。每個Java線程都擁有一個私有的Java虛擬機棧,隨着線程開始而創建,結束而銷燬。Java虛擬機棧中存儲的是棧幀(Stack Frame)。每個方法被執行的時候都會同時在Java虛擬機棧中創建一個棧幀,在棧幀內存儲了方法的局部變量,方法返回值,動態鏈接和分派的異常。每一個方法從調用、執行完成的過程就是Java虛擬機棧中的棧幀從入棧到出棧的過程。

Java虛擬機棧中的每個棧幀自內存分配完成後,可以認爲其棧幀大小是固定不變的。這是因爲,局部變量表所需的內存空間在編譯期間可知,並且在編譯期間分配完成。在方法運行期間不會改變局部變量表的大小。

局部變量表包括:1)基本數據類型(boolean, byte, char, short, int, float, long, double);2)對象引用;3)returnAddress類型。

在Java虛擬機規範中,規定的了Java虛擬機棧可以固定大小或者動態擴展其虛擬機棧大小,並且定義了兩種異常情況:

1)當虛擬機棧的大小超過限定大小,JVM拋出StackOverflowError。

2)若虛擬機棧可動態擴容,但已無法爲其繼續動態擴容。或者無法爲一個新的Java線程新建一個虛擬機棧,則拋出OutOfMemoryError。

下面是一段示例程序,創建了一個可能的虛擬機棧內存溢出的情況:

/**
 * <h5>StackOverflowError</h5>
 * 
 * <pre>
 * 當線程請求的虛擬機棧的大小超過限定大小,將拋出<code>java.lang.StackOverflowError</code>錯誤。
 * </pre>
 * 
 * @author yongqing_wang
 */
public class StackOverflowErrorTest {

    public static void main(String[] args) {
        StackOverflowErrorTest sofe = new StackOverflowErrorTest();
        sofe.recurse();
    }

    public void recurse() {
        recurse();
    }
}

Exception in thread "main" java.lang.StackOverflowError
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)

4. 線程隔離數據區--本地方法棧(Native Method Stacks)

本地方法棧與虛擬機棧的功能幾乎完全一致,其區別是執行的方法不同。Java虛擬機棧爲執行Java方法(字節碼)服務,而本地方法棧爲執行Native方法服務。對於本地方法棧,JVM規範沒有對其做任何規定,因此也無法在《JVM虛擬機規範》中找到,其隨着具體虛擬機的實現不同而不同。也可以將本地方法棧與虛擬機棧合併。同樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError。其發生情況與虛擬機棧類似。


5. 線程共享數據區--Java 堆(Heap)

Java堆是Java中最重要的一塊,其對於Java的運行性能影響也最爲重要。Java堆爲線程共享數據區域,在JVM啓動時創建,其唯一目的就是存放Java對象實例,在《JVM虛擬機規範》中規定,所有的對象實例和數組都在這裏分配內存。(現在這已經不是絕對,但在此不做討論)

Java堆的管理直接影響內存利用率,訪問效率等影響Java運行效率的直接參數。因此,Java堆由自動存儲管理系統管理(auto storage management system)管理(garbage collector就是其一種實現的名稱)其分配、回收和整理。

規範中規定了Java堆可固定大小,也可以由程序員or用戶自己分配初始化大小,也可以對堆進行動態擴容,其異常情況:

1)當計算需要更多堆空間時,堆無法爲其分配,JVM拋出OOM異常。

下面是一段示例程序,演示了Java堆異常的可能情況:

public class HeapOOMTest {

    public static void main(String[] args) {
        List<String> huge = new ArrayList<String>();

        while (true) {
            String tmp = new String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
            huge.add(tmp);
        }
    }
}


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2760)
	at java.util.Arrays.copyOf(Arrays.java:2734)
	at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
	at java.util.ArrayList.add(ArrayList.java:351)
	at jvm.jmm.overflow.HeapOOMTest.main(HeapOOMTest.java:13)


6. 線程共享數據區--方法區(Method Area)

(方法區對於我而言比較陌生,其原因是對Java的學習還不夠深入,在這裏只做整理及記錄工作。)

方法區是Java堆區的一個邏輯組成部分,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,可以不受GC的管理。(在《JVM虛擬機規範》中是這麼描述方法區的:The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in a UNIX process.It stores per-class structures such as the runtime constant pool, field and method data, and the code for methods and constructors, including the special methods(§3.9) used in class and instance initialization and interface type initialization. )

規範中規定了方法區可固定大小,也可以由程序員or用戶自己分配初始化大小,也可以對方法區進行動態擴容,其異常情況:

1)當方法區內存不足以滿足分配請求,JVM拋出OOM異常。


6.1 運行時常量池(Runtime Constant Pool)

運行時常量池是方法區的一部分,用於存放編譯期生成的各種數字文字量(numeric literals)和符號引用。運行時常量池提供類似於符號表作用,只是運行時常量池的數據類型遠廣泛於傳統的符號表。運行時常量池在類(class)或者接口(interface)被JVM創建時構建。

我們可以通過javap -p -verbose來查看一個類的運行時常量池:

Compiled from "RuntimeConstantPoolOverflowErrorTest.java"
public class jvm.jmm.overflow.RuntimeConstantPoolOverflowErrorTest extends java.lang.Object
  SourceFile: "RuntimeConstantPoolOverflowErrorTest.java"
  minor version: 0
  major version: 50
  Constant pool:
const #1 = Method	#8.#30;	//  java/lang/Object."<init>":()V
const #2 = class	#31;	//  java/util/ArrayList
const #3 = Method	#2.#30;	//  java/util/ArrayList."<init>":()V
const #4 = Method	#32.#33;	//  java/lang/String.valueOf:(I)Ljava/lang/String;
const #5 = Method	#32.#34;	//  java/lang/String.intern:()Ljava/lang/String;
const #6 = InterfaceMethod	#35.#36;	//  java/util/List.add:(Ljava/lang/Object;)Z
const #7 = class	#37;	//  jvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest
const #8 = class	#38;	//  java/lang/Object
const #9 = Asciz	<init>;
const #10 = Asciz	()V;
const #11 = Asciz	Code;
const #12 = Asciz	LineNumberTable;
const #13 = Asciz	LocalVariableTable;
const #14 = Asciz	this;
const #15 = Asciz	Ljvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest;;
const #16 = Asciz	main;
const #17 = Asciz	([Ljava/lang/String;)V;
const #18 = Asciz	args;
const #19 = Asciz	[Ljava/lang/String;;
const #20 = Asciz	list;
const #21 = Asciz	Ljava/util/List;;
const #22 = Asciz	i;
const #23 = Asciz	I;
const #24 = Asciz	LocalVariableTypeTable;
const #25 = Asciz	Ljava/util/List<Ljava/lang/String;>;;
const #26 = Asciz	StackMapTable;
const #27 = class	#39;	//  java/util/List
const #28 = Asciz	SourceFile;
const #29 = Asciz	RuntimeConstantPoolOverflowErrorTest.java;
const #30 = NameAndType	#9:#10;//  "<init>":()V
const #31 = Asciz	java/util/ArrayList;
const #32 = class	#40;	//  java/lang/String
const #33 = NameAndType	#41:#42;//  valueOf:(I)Ljava/lang/String;
const #34 = NameAndType	#43:#44;//  intern:()Ljava/lang/String;
const #35 = class	#39;	//  java/util/List
const #36 = NameAndType	#45:#46;//  add:(Ljava/lang/Object;)Z
const #37 = Asciz	jvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest;
const #38 = Asciz	java/lang/Object;
const #39 = Asciz	java/util/List;
const #40 = Asciz	java/lang/String;
const #41 = Asciz	valueOf;
const #42 = Asciz	(I)Ljava/lang/String;;
const #43 = Asciz	intern;
const #44 = Asciz	()Ljava/lang/String;;
const #45 = Asciz	add;
const #46 = Asciz	(Ljava/lang/Object;)Z;

規範中規定了運行時常量池的異常情況:

1)當創建一個類或者接口時,運行時常量池需要的內存空間不能夠在方法區分配,JVM拋出OOM異常。

下面是一段示例程序,模擬了運行時常量池的溢出:

/**
 * <h5>Runtime Constant Pool overflow.</h5>
 * 
 * <pre>
 * This is because String.intern() returns a constant string using
 * native method.
 * </pre>
 * 
 * @author yongqing_wang
 */
public class RuntimeConstantPoolOverflowErrorTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at jvm.jmm.overflow.RuntimeConstantPoolOverflowErrorTest.main(RuntimeConstantPoolOverflowErrorTest.java:24)

7. 直接內存

直接內存並不是JVM運行時數據區的一部分,也沒有在《JVM虛擬機規範》中進行定義。它可以是作爲一種堆外緩存機制存在,在某些場景下可以顯著提高性能。

但由於這部分直接內存的分配不會受到Java堆大小的限制,以此在設置JVM參數時需要考慮其存在,防止內存實際分配區域超過實際內存區域而導致的OOM異常。


參考文獻:

[1] 《深入理解Java虛擬機》 http://book.douban.com/subject/6522893/

[2] 《Java虛擬機規範》  http://java.sun.com/docs/books/jvms/second_edition/html/Overview.doc.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章