深刻理解JVM之JVM內存區域與內存分配

  在學習jvm的內存分配的時候,看到的這篇博客,該博客對jvm的內存分配總結的很好,同時也利用jvm的內存模型解釋了java程序中有關參數傳遞的問題。html

  博客出處: http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referraljava

  看了此博客後,發現應該去深刻學習下jvm的內存模型,就是去認真學習下《深刻理解Java虛擬機》,其內容可能會《深度探索c++對象模型》相似,解釋了java中對象的在內存中的模型,學習了對象的內存模型後,對理解多態、參數傳遞等的理解都有幫助。c++

  前言:這是一篇關於JVM內存區域的文章,由網上一些有關這方面的文章和《深刻理解Java虛擬機》整理而來,因此會有些類同的地方,也不能保證我本身寫的比其餘網上的和書本上的要好,也不可能會這樣。寫博客的目的是爲了我的對這方面本身理解的分享與我的的積累,因此有寫錯的地方多多指教。bootstrap

  看到深刻兩字,相信不少的JAVA初學者都會直接忽略這樣的文章,其實關於JVM內存區域的知識對於初學者來講實際上是很重要的,瞭解Java內存分配的原理,這對於之後JAVA的學習會有更深入的理解,這是我我的的見解。數組

  先來看看JVM運行時候的內存區域jvm

  大多數 JVM 將內存區域劃分爲 Method Area(Non-Heap)(方法區),Heap(堆),Program Counter Register(程序計數器), VM Stack(虛擬機棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧),其中Method AreaHeap是線程共享的,VMStack,Native Method Stack 和Program Counter Register是非線程共享的。爲何分爲線程共享和非線程共享的呢?請繼續往下看。學習

  首先咱們熟悉一下一個通常性的 Java 程序的工做過程。一個 Java 源程序文件,會被編譯爲字節碼文件(以 class 爲擴展名),每一個java程序都須要運行在本身的JVM上,而後告知 JVM 程序的運行入口,再被 JVM 經過字節碼解釋器加載運行。那麼程序開始運行後,都是如何涉及到各內存區域的呢?ui

  歸納地說來,JVM初始運行的時候都會分配好Method Area(方法區)Heap(堆),而JVM 每遇到一個線程,就爲其分配一個Program Counter Register(程序計數器), VM Stack(虛擬機棧)和Native Method Stack (本地方法棧),當線程終止時,三者(虛擬機棧,本地方法棧和程序計數器)所佔用的內存空間也會被釋放掉。這也是爲何我把內存區域分爲線程共享和非線程共享的緣由,非線程共享的那三個區域的生命週期與所屬線程相同,而線程共享的區域與JAVA程序運行的生命週期相同,因此這也是系統垃圾回收的場所只發生在線程共享的區域(實際上對大部分虛擬機來講知發生在Heap上)的緣由。this

  

1.  程序計數器spa

  程序計數器是一塊較小的內存區域,做用能夠看作是當前線程執行的字節碼的位置指示器。分支、循環、跳轉、異常處理和線程恢復等基礎功能都須要依賴這個計算器來完成,很少說。

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 中分配必定的內存保存對象實例。而對象實例在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的動態連接中起了核心做用.
  頗有用且重要關於常量池的擴展:Java常量池詳解 http://www.cnblogs.com/DreamSea/archive/2011/11/20/2256396.html
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類的實例d一、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.類的成員變量在不一樣對象中各不相同,都有本身的存儲空間(成員變量在堆中的對象中)。而類的方法倒是該類的全部對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不佔用內存。

相關文章
相關標籤/搜索