圖解JVM 內存分配

1.簡介

現摘錄一段Java5內存管理白皮書中的一段話:

One strength of the Java™ 2 Platform, Standard Edition (J2SE™) is that it performs automatic memory
management, thereby shielding the developer from the complexity of explicit memory management.

由上面我們可以知道,Java平臺採用了自動的內存管理方式,那麼JVM是如何進行內存管理的呢?

Java的內存管理實際上包含兩個方面:

1).Java內存分配

2).Java的內存回收

搞清楚了這兩個問題,Java的內存管理就清楚了。

寫這篇文章之前,問了自己一個問題:

Java採用了自動的內存管理方式,程序員實際上不用關心內存管理的細節,那爲什麼我們仍然需要了解Java內存管理的內幕?

原因有三:

1.瞭解Java內存管理的細節,有助於程序員編寫出性能更好的程序。比如,在新的線程創建時,JVM會爲每個線程創建一個專屬的棧(stack),其棧是先進後出的數據結構,這種方式的特點,讓程序員編程時,必須特別注意遞歸方法要儘量少使用,另外棧的大小也有一定的限制,如果過多的遞歸,容易導致stack overflow。

2.瞭解Java內存管理的細節,一旦內存管理出現問題,有助於找到問題的根本原因所在。

3.瞭解Java內存管理的內幕,有助於優化JVM,從而使得自己的應用獲得最佳的性能體驗。

 

JVM的體系結構如下:

如下圖所示,JVM的體系結構包含幾個主要的子系統和內存區:

類裝載子系統 ,負責把類從文件系統中裝入內存

GC子系統 ,垃圾收集器的主要工作室自動回收不再運行的程序引用對象所佔用的內存,此外,它還可能負責那些還在使用的對象,以減少的堆碎片。

內存區 ,用於存儲字節碼,程序運行時創建的對象,傳遞給方法的參數,返回值,局部變量和中間計算結果。

執行引擎: 
1、最簡單的:一次性解釋字節碼。 
2、快,但消耗內存的:“即時編譯器”,第一次被執行的字節碼會被編譯成機器代碼,放入緩存,以後調用可以重用。 
3、自適應優化器,虛擬機開始的時候會解釋字節碼,但是會監視運行中程序的活動,並記錄下使用最頻繁的代碼段。程序運行的時候,虛擬機只把使用最頻繁的代碼編譯成本地代碼,其他的代碼由於使用的並不頻繁,繼續保留爲字節碼--由虛擬機繼續解釋他們。一般可以使java虛擬機80%~90%的時間裏執行被優化過的本地代碼,只需要編譯10%~20%對性能優影響的代碼。 
4、由硬件芯片組成,他用本地方法執行java字節碼,這種執行引擎實際上是內嵌在芯片裏的。

2. Java的內存分配

在Java程序運行過程中,JVM定義了各種區域用於存儲運行時數據。其中的有些數據區域在JVM啓動時創建,並只在JVM退出時銷燬。其它的數據區域與每個線程相關。這些數據區域,在線程創建時創建,在線程退出時銷燬。

2.1 程序計數器寄存器(The pc Register)

JVM支持多個線程同時運行。每個JVM都有自己的程序計數器。在任何一個點,每個JVM線程執行單個方法的代碼,這個方法是線程的當前方法。如果方法不是native的,程序計數器寄存器包含了當前執行的JVM指令的地址,如果方法是 native的,程序計數器寄存器的值不會被定義。 JVM的程序計數器寄存器的寬度足夠保證可以持有一個返回地址或者native的指針。

2.2 棧

1)棧與線程

JVM是基於棧的虛擬機.JVM爲每個新創建的線程都分配一個棧.也就是說,對於一個Java程序來說,它的運行就是通過對棧的操作來完成的。棧以幀爲單位保存線程的狀態。JVM對棧只進行兩種操作:以幀爲單位的壓棧和出棧操作。

我們知道,某個線程正在執行的方法稱爲此線程的當前方法.我們可能不知道,當前方法使用的幀稱爲當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧裏新壓入一個幀。這個幀自然成爲了當前幀.在此方法執行期間,這個幀將用來保存參數,局部變量,中間計算過程和其他數據.這個幀在這裏和編譯原理中的活動紀錄的概念是差不多的.

從Java的這種分配機制來看,堆棧又可以這樣理解:棧(Stack)是操作系統在建立某個進程時或者線程(在支持多線程的操作系統中是線程)爲這個線程建立的存儲區域,該區域具有先進後出的特性。

2)棧中的方法調用

嵌套方法的出棧和入棧示意圖: 

上圖中描述了嵌套方法時,stack的內存分配圖,由上面可以知道,當嵌套方法調用時,嵌套越深,stack的內存就越晚才能釋放,因此,在實際開發過程中,不推薦大家使用遞歸來進行方法的調用,遞歸很容易導致stack flow。

非嵌套方法的出棧入棧過程:

2.3 堆

每一個Java應用都唯一對應一個JVM實例,每一個實例唯一對應一個堆。應用程序在運行中所創建的所有類實例或數組都放在這個堆中,並由應用所有的線程共享.跟C/C++不同,Java中分配堆內存是自動初始化的。Java中所有對象的存儲空間都是在堆中分配的,但是這個對象的引用卻是在堆棧中分配,也就是說在建立一個對象時從兩個地方都分配內存,在堆中分配的內存實際建立這個對象,而在堆棧中分配的內存只是一個指向這個堆對象的指針(引用)而已。

2.4 堆和棧的區別

【下面的部分屬於摘抄,描述比較好】

1. 棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方 。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。

2. 棧的優勢是,存取速度比堆要快 ,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享,詳見第3點。堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢。

3. Java中的數據類型有兩種:  
    一種是基本類型(primitive types),
 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的原因,就存在於棧中。
另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義: 
    int a = 3; 
    int b = 3;
編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。 
    特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那麼另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。如上例,我們定義完a與b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。 
    另一種是包裝類數據 ,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要佔用更多的時間。

4. String是一個特殊的包裝類數據 。即可以用String str = new String("abc");的形式來創建,也可以用String str = "abc";的形式來創建(作爲對比,在JDK 5.0之前,你從未見過Integer i = 3;的表達式,因爲類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是可以的!因爲編譯器在後臺進行Integer i = new Integer(3)的轉換)。前者是規範的類的創建過程,即在Java中,一切都是對象,而對象是類的實例,全部通過new()的形式來創建。Java中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過new()來創建的,而getInstance()向外部隱藏了此細節。那爲什麼在String str = "abc";中,並沒有通過new()來創建實例,是不是違反了上述原則?其實沒有。 
5. 關於String str = "abc"的內部工作。
Java內部將此語句轉化爲以下幾個步驟: 
(1)先定義一個名爲str的對String類的對象引用變量:String str; 
(2)在棧中查找有沒有存放值爲"abc"的地址,如果沒有,則開闢一個存放字面值爲"abc"的地址,接着創建一個新的String類的對象o,並將o的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象o。如果已經有了值爲"abc"的地址,則查找對象o,並返回o的地址。 
(3)將str指向對象o的地址。 
    值得注意的是,一般String類中字符串值都是直接存值的。但像String str = "abc";這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用! 
   爲了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。 
   String str1 = "abc"; 
   String str2 = "abc"; 
   System.out.println(str1==str2); //true 
注意,我們這裏並不用str1.equals(str2);的方式,因爲這將比較兩個字符串的值是否相等。“==”號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這裏要看的是,str1與str2是否都指向了同一個對象。結果說明,JVM創建了兩個引用str1和str2,但只創建了一個對象,而且兩個引用都指向了這個對象。 
    我們再來更進一步,將以上代碼改成: 
    String str1 = "abc"; 
    String str2 = "abc"; 
    str1 = "bcd"; 
    System.out.println(str1 + "," + str2); //bcd, abc 
    System.out.println(str1==str2); //false 
這就是說,賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們將str1的值改爲"bcd"時,JVM發現在棧中沒有存放該值的地址,便開闢了這個地址,並創建了一個新的對象,其字符串的值指向這個地址。 
事實上,String類被設計成爲不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運行時根據新值悄悄創建了一個新對象,然後將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟佔用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。 
    再修改原來代碼: 
    String str1 = "abc"; 
    String str2 = "abc"; 
    str1 = "bcd"; 
    String str3 = str1; 
    System.out.println(str3); //bcd 
    String str4 = "bcd"; 
    System.out.println(str1 == str4); //true 
str3這個對象的引用直接指向str1所指向的對象(注意,str3並沒有創建新對象)。當str1改完其值後,再創建一個String的引用str4,並指向因str1修改值而創建的新的對象。可以發現,這回str4也沒有創建新的對象,從而再次實現棧中數據的共享。 
    我們再接着看以下的代碼。 
    String str1 = new String("abc"); 
    String str2 = "abc"; 
    System.out.println(str1==str2); //false 
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。 
String str1 = "abc"; 
String str2 = new String("abc"); 
System.out.println(str1==str2); //false 
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。 
以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。 
6. 數據類型包裝類的值不可修改。不僅僅是String類的值不可修改,所有的數據類型包裝類都不能更改其內部的值。
7. 結論與建議:
(1)我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認爲,我們創建了String類的對象str。擔心陷阱!對象可能並沒有被創建!唯一可以肯定的是,指向String類的引用被創建了。至於這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過new()方法來顯要地創建一個新的對象。因此,更爲準確的說法是,我們創建了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值爲"abc"的String類。清醒地認識到這一點對排除程序中難以發現的bug是很有幫助的。 
(2)使用String str = "abc";的方式,可以在一定程度上提高程序的運行速度,因爲JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對於String str = new String("abc");的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裏實現是否應用了這個模式,不得而知。 
(3)當比較包裝類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用“==”。 
(4)由於String類的immutable性質,當String變量需要經常變換其值時,應該考慮使用StringBuffer類,以提高程序效率。

2.5 方法區

JVM有一個被所有的線程共享方法區。方法區類似於傳統語言的編譯後代碼的存儲區,或者UNIX進程中的text段。它存儲每個類結構例如常量池(constant pool),成員字段域和方法和構造函數,包含類和實例初始化和接口類型類型中用到的特殊方法的代碼。

方法區在虛擬機啓動時創建。儘管方法區在邏輯上時heap的一部分,簡單的實現仍然可以選擇對它既不回收也不壓縮。

The Java virtual machine has a method area that is shared among all Java virtual machine threads. 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.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This version of the Java virtual machine specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java virtual machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

 

原創作者: tomyz0223,摘自javaeye  高級語言虛擬機 Java內存管理基礎篇- Java內存分配

轉載自:http://www.mapfilm.com/articles/2011/06/13/1307954329185.html

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