1. 程式人生 > >深入理解JVM之JVM記憶體區域與記憶體分配

深入理解JVM之JVM記憶體區域與記憶體分配

部落格出處: http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referral

先來看看JVM執行時候的記憶體區域

  大多數 JVM 將記憶體區域劃分為 Method Area(Non-Heap)(方法區),Heap(堆),Program Counter Register(程式計數器)VM Stack(虛擬機器棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧),其中Method AreaHeap是執行緒共享的,VM Stack,Native Method Stack 和Program Counter Register

是非執行緒共享的。為什麼分為執行緒共享和非執行緒共享的呢?請繼續往下看。

  首先我們熟悉一下一個一般性的 Java 程式的工作過程。一個 Java 源程式檔案,會被編譯為位元組碼檔案(以 class 為副檔名),每個java程式都需要執行在自己的JVM上,然後告知 JVM 程式的執行入口,再被 JVM 通過位元組碼直譯器載入執行。那麼程式開始執行後,都是如何涉及到各記憶體區域的呢?

  概括地說來,JVM初始執行的時候都會分配好Method Area(方法區)Heap(堆),而JVM 每遇到一個執行緒,就為其分配一個Program Counter Register(程式計數器)VM Stack(虛擬機器棧)和Native Method Stack (本地方法棧),

當執行緒終止時,三者(虛擬機器棧,本地方法棧和程式計數器)所佔用的記憶體空間也會被釋放掉。這也是為什麼我把記憶體區域分為執行緒共享和非執行緒共享的原因,非執行緒共享的那三個區域的生命週期與所屬執行緒相同,而執行緒共享的區域與JAVA程式執行的生命週期相同,所以這也是系統垃圾回收的場所只發生線上程共享的區域(實際上對大部分虛擬機器來說知發生在Heap上)的原因。

1.  程式計數器

  程式計數器是一塊較小的記憶體區域,作用可以看做是當前執行緒執行的位元組碼的位置指示器。分支、迴圈、跳轉、異常處理和執行緒恢復等基礎功能都需要依賴這個計算器來完成,不多說。

2.VM Strack

  先來了解下JAVA指令的構成:

  JAVA指令由 操作碼 (方法本身)和 運算元(方法內部變數) 組成。   

    1)方法本身是指令的操作碼部分,儲存在Stack中;

    2)方法內部變數(區域性變數)作為指令的運算元部分,跟在指令的操作碼之後,儲存在Stack中(實際上是簡單型別(int,byte,short 等儲存在Stack中),物件型別在Stack中儲存地址,在Heap 中儲存值

  虛擬機器棧也叫棧記憶體,是線上程建立時建立,它的生命期是跟隨執行緒的生命,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束,該棧就 Over,所以不存在垃圾回收。也有一些資料翻譯成JAVA方法棧,大概是因為它所描述的是java方法執行的記憶體模型,每個方法執行的同時建立幀棧(Strack Frame)用於儲存區域性變量表(包含了對應的方法引數和區域性變數),操作棧(Operand Stack,記錄出棧、入棧的操作),動態連結、方法出口等資訊,每個方法被呼叫直到執行完畢的過程,對應這幀棧在虛擬機器棧的入棧和出棧的過程。

  區域性變量表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件的引用(reference型別,不等同於物件本身,根據不同的虛擬機器實現,可能是一個指向物件起始地址的引用指標,也可能是一個代表物件的控制代碼或者其他與物件相關的位置)和 returnAdress型別(指向下一條位元組碼指令的地址)。區域性變量表所需的記憶體空間在編譯期間完成分配,在方法在執行之前,該區域性變量表所需要的記憶體空間是固定的,執行期間也不會改變。

  棧幀是一個記憶體區塊,是一個數據集,是一個有關方法(Method)和執行期資料的資料集,當一個方法 A 被呼叫時就產生了一個棧幀 F1,並被壓入到棧中,A 方法又呼叫了 B 方法,於是產生棧幀 F2 也被壓入棧,執行完畢後,先彈出 F2棧幀,再彈出 F1 棧幀,遵循“先進後出”原則。光說比較枯燥,我們看一個圖來理解一下 Java棧,如下圖所示:

 3.Heap

  Heap(堆)是JVM的記憶體資料區。Heap 的管理很複雜,是被所有執行緒共享的記憶體區域,在JVM啟動時候建立,專門用來儲存物件的例項。在Heap 中分配一定的記憶體來儲存物件例項,實際上也只是儲存物件例項的屬性值,屬性的型別和物件本身的型別標記等,並不儲存物件的方法(以幀棧的形式儲存在Stack中)而物件例項在Heap 中分配好以後,需要在Stack中儲存一個4位元組的Heap 記憶體地址,用來定位該物件例項在Heap 中的位置,便於找到該物件例項,是垃圾回收的主要場所。java堆處於物理不連續的記憶體空間中,只要邏輯上連續即可。

4.Method Area

  Object Class Data(載入類的類定義資料) 是儲存在方法區的。除此之外,常量靜態變數、JIT(即時編譯器)編譯後的程式碼也都在方法區。正因為方法區所儲存的資料與堆有一種類比關係,所以它還被稱為 Non-Heap。方法區也可以是記憶體不連續的區域組成的,並且可設定為固定大小,也可以設定為可擴充套件的,這點與堆一樣。

  垃圾回收在這個區域會比較少出現,這個區域記憶體回收的目的主要針對常量池的回收和類的解除安裝。

5.執行時常量池(Runtime Constant Pool)

  方法區內部有一個非常重要的區域,叫做執行時常量池(Runtime Constant Pool,簡稱 RCP)。在位元組碼檔案(Class檔案)中,除了有類的版本、欄位、方法、介面等先關資訊描述外,還有常量池(Constant Pool Table)資訊,用於儲存編譯器產生的字面量和符號引用。這部分內容在類被載入後,都會儲存到方法區中的RCP。值得注意的是,執行時產生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產生的常量。

  常量池就是這個型別用到的常量的一個有序集合。包括直接常量(基本型別,String)對其他型別、方法、欄位的符號引用.例如:

◆類和介面的全限定名;

◆欄位的名稱和描述符;

◆方法和名稱和描述符。

  池中的資料和陣列一樣通過索引訪問。由於常量池包含了一個型別所有的對其他型別、方法、欄位的符號引用,所以常量池在Java的動態連結中起了核心作用.

6.Native Method Stack

與VM Strack相似,VM Strack為JVM提供執行JAVA方法的服務,Native Method Stack則為JVM提供使用native 方法的服務。

7.直接記憶體區

  直接記憶體區並不是 JVM 管理的記憶體區域的一部分,而是其之外的。該區域也會在 Java 開發中使用到,並且存在導致記憶體溢位的隱患。如果你對 NIO 有所瞭解,可能會知道 NIO 是可以使用 Native Methods 來使用直接記憶體區的。

小結:

  •   在此,你對JVM的記憶體區域有了一定的理解,JVM記憶體區域可以分為執行緒共享和非執行緒共享兩部分,執行緒共享的有堆和方法區,非執行緒共享的有虛擬機器棧,本地方法棧和程式計數器。

8.JVM執行原理 例子

以上都是純理論,我們舉個例子來說明 JVM 的執行原理,我們來寫一個簡單的類,程式碼如下:

複製程式碼

 1 public class JVMShowcase {  
 2 //靜態類常量,  
 3 public final static String ClASS_CONST = "I'm a Const";  
 4 //私有例項變數  
 5 private int instanceVar=15;  
 6 public static void main(String[] args) {  
 7 //呼叫靜態方法  
 8 runStaticMethod();  
 9 //呼叫非靜態方法  
10 JVMShowcase showcase=new JVMShowcase();  
11 showcase.runNonStaticMethod(100);  
12 }  
13 //常規靜態方法  
14 public static String runStaticMethod(){  
15 return ClASS_CONST;  
16 }  
17 //非靜態方法  
18 public int runNonStaticMethod(int parameter){  
19 int methodVar=this.instanceVar * parameter;  
20 return methodVar;  
21 }  
22 }  

複製程式碼

這個類沒有任何意義,不用猜測這個類是做什麼用,只是寫一個比較典型的類,然後我們來看

看 JVM 是如何執行的,也就是輸入 java JVMShow 後,我們來看 JVM 是如何處理的:

     第 1 步 、向作業系統申請空閒記憶體。JVM 對作業系統說“給我 64M(隨便模擬資料,並不是真實資料) 空閒記憶體”,於是,JVM 向作業系統申請空閒記憶體作系統就查詢自己的記憶體分配表,找了段 64M 的記憶體寫上“Java 佔用”標籤,然後把記憶體段的起始地址和終止地址給 JVM,JVM 準備載入類檔案。

     第 2 步,分配記憶體記憶體。JVM 分配記憶體。JVM 獲得到 64M 記憶體,就開始得瑟了,首先給 heap 分個記憶體,然後給棧記憶體也分配好。

     第 3 步,檔案檢查和分析class 檔案。若發現有錯誤即返回錯誤。

     第 4 步,載入類。載入類。由於沒有指定載入器,JVM 預設使用 bootstrap 載入器,就把 rt.jar 下的所有類都載入到了堆類存的Method Area,JVMShow 也被載入到記憶體中。我們來看看Method Area區域,如下圖:(這時候包含了 main 方法和 runStaticMethod方法的符號引用,因為它們都是靜態方法,在類載入的時候就會載入

Heap 是空,Stack 是空,因為還沒有物件的新建和執行緒被執行。

      第 5 步、執行方法。執行 main 方法。執行啟動一個執行緒,開始執行 main 方法,在 main 執行完畢前,方法區如下圖所示:

public final static String ClASS_CONST = "I'm a Const";  

     在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被訪問時產生的(runStaticMethod方法內部)。

     堆記憶體中有兩個物件 object 和 showcase 物件,如下圖所示:(執行了JVMShowcase showcase=new JVMShowcase();  )

為什麼會有 Object 物件呢?是因為它是 JVMShowcase 的父類,JVM 是先初始化父類,然後再初始化子類,甭管有多少個父類都初始化。

在棧記憶體中有三個棧幀,如下圖所示:

於此同時,還建立了一個程式計數器指向下一條要執行的語句。

第 6 步,釋放記憶體。釋放記憶體。執行結束,JVM 向作業系統傳送訊息,說“記憶體用完了,我還給你”,執行結束。

--------------------------------------------------------------------------------------------

現在來看JVM記憶體是如何分配的,該部分轉載來自 http://blog.csdn.net/shimiso/article/details/8595564

預備知識:

1.一個Java檔案,只要有main入口方法,我們就認為這是一個Java程式,可以單獨編譯執行。

2.無論是普通型別的變數還是引用型別的變數(俗稱例項),都可以作為區域性變數,他們都可以出現在棧中。只不過普通型別的變數在棧中直接儲存它所對應的值,而引用型別的變數儲存的是一個指向堆區的指標,通過這個指標,就可以找到這個例項在堆區對應的物件。因此,普通型別變數只在棧區佔用一塊記憶體,而引用型別變數要在棧區和堆區各佔一塊記憶體。

示例:(以下所有例項中,是根據需要對於棧記憶體中的幀棧簡化成了只有區域性變量表,實際上由上面對幀棧的介紹知道不僅僅只有這些資訊,同理堆記憶體也一樣)

1.JVM自動尋找main方法,執行第一句程式碼,建立一個Test類的例項,在棧中分配一塊記憶體,存放一個指向堆區物件的指標110925。

2.建立一個int型的變數date,由於是基本型別,直接在棧中存放date對應的值9。

3.建立兩個BirthDate類的例項d1、d2,在棧中分別存放了對應的指標指向各自的物件。他們在例項化時呼叫了有引數的構造方法,因此物件中有自定義初始值。

呼叫test物件的change1方法,並且以date為引數。JVM讀到這段程式碼時,檢測到i是區域性變數,因此會把i放在棧中,並且把date的值賦給i。

把1234賦給i。很簡單的一步。

change1方法執行完畢,立即釋放區域性變數i所佔用的棧空間。

呼叫test物件的change2方法,以例項d1為引數。JVM檢測到change2方法中的b引數為區域性變數,立即加入到棧中,由於是引用型別的變數,所以b中儲存的是d1中的指標,此時b和d1指向同一個堆中的物件。在b和d1之間傳遞是指標。

change2方法中又例項化了一個BirthDate物件,並且賦給b。在內部執行過程是:在堆區new了一個物件,並且把該物件的指標儲存在棧中的b對應空間,此時例項b不再指向例項d1所指向的物件,但是例項d1所指向的物件並無變化,這樣無法對d1造成任何影響。

change2方法執行完畢,立即釋放區域性引用變數b所佔的棧空間,注意只是釋放了棧空間,堆空間要等待自動回收。

呼叫test例項的change3方法,以例項d2為引數。同理,JVM會在棧中為區域性引用變數b分配空間,並且把d2中的指標存放在b中,此時d2和b指向同一個物件。再呼叫例項b的setDay方法,其實就是呼叫d2指向的物件的setDay方法。

呼叫例項b的setDay方法會影響d2,因為二者指向的是同一個物件。

change3方法執行完畢,立即釋放區域性引用變數b。

以上就是Java程式執行時記憶體分配的大致情況。其實也沒什麼,掌握了思想就很簡單了。無非就是兩種型別的變數:基本型別和引用型別。二者作為區域性變數,都放在棧中,基本型別直接在棧中儲存值,引用型別只儲存一個指向堆區的指標,真正的物件在堆裡。作為引數時基本型別就直接傳值,引用型別傳指標。

小結:

1.分清什麼是例項什麼是物件。Class a= new Class();此時a叫例項,而不能說a是物件。例項在棧中,物件在堆中,操作例項實際上是通過例項的指標間接操作物件。多個例項可以指向同一個物件。

2.棧中的資料和堆中的資料銷燬並不是同步的。方法一旦結束,棧中的區域性變數立即銷燬,但是堆中物件不一定銷燬。因為可能有其他變數也指向了這個物件,直到棧中沒有變數指向堆中的物件時,它才銷燬,而且還不是馬上銷燬,要等垃圾回收掃描時才可以被銷燬。

3.以上的棧、堆、程式碼段、資料段等等都是相對於應用程式而言的。每一個應用程式都對應唯一的一個JVM例項,每一個JVM例項都有自己的記憶體區域,互不影響。並且這些記憶體區域是所有執行緒共享的。這裡提到的棧和堆都是整體上的概念,這些堆疊還可以細分。

4.類的成員變數在不同物件中各不相同,都有自己的儲存空間(成員變數在堆中的物件中)。而類的方法卻是該類的所有物件共享的,只有一套,物件使用方法的時候方法才被壓入棧,方法不使用則不佔用記憶體。