深入理解java虛擬機 -- java虛擬機內存結構(1)

JVMåå­ç»æåJavaåå­æ¨¡å

目錄

學習目標:

JVM的作用:

java代碼編譯執行過程

1、程序計數器(Program Counter Register):

2、虛擬機棧(JVM Stack):

3、本地方法棧(Native Method Statck):

4、堆區(Heap):

5、方法區(Method Area):

6、運行時常量池:

7、直接內存(Direct Memory):


 

 

學習目標:

  • 由 JVM 引發的故障問題,無論在我們開發過程中還是生產環境下都是非常常見的,所有掌握好jvm,可以幫我本排查故障。
  • OutOfMemoryError(OOM) 內存溢出問題,Tomcat 容器中加載項目過多導致的 OOM 問題。
  • 定位JVM哪裏發生內存溢出了,爲什麼會內存溢出呢?如何監控 JVM運行。
  • JVM性能調優,JVM的內存區域劃分,內存分配比例,並且可以通過JVM參數來反向定位代碼問題,優化代碼結構。
  • 從JVM本質上了解線程併發安全的實現原理,以及與操作系統如何結合。Java虛擬機(JVM) 處在覈心的位置,是程序與底層操作系統、硬件無關的關鍵。

 

JVM的作用:

  1. 編譯代碼,將java代碼編譯爲字節碼也就是class文件。
  2. Java虛擬機(JVM) 處在覈心的位置,是程序與底層操作系統、硬件無關的關鍵。
  3. JVM的下方是移植接口,移植接口由兩部分組成:適配器和Java操作系統, 其中依賴於平臺的部分稱爲適配器,JVM 通過移植接口在具體的平臺和操作系統上實現。
  4. JVM 的上方是Java的基本類庫和擴展類庫以及它們的API, 利用Java API編寫的應用程序(application) 和小程序(Java applet) 可以在任何Java平臺上運行而無需考慮底層平臺
  5. Java虛擬機(JVM)實現了程序與操作系統的分離,從而實現了Java 的跨平臺

 

先上一張圖片:

 根據《Java虛擬機規範》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時數據區域

  • 黃色 部分爲線程共享區域。
  • 藍色 部分爲線程私有部分。

首先說一下

java代碼編譯執行過程

  1.源碼編譯:通過Java源碼編譯器將Java代碼編譯成JVM字節碼(.class文件)

  2.類加載:通過ClassLoader及其子類來完成JVM的類加載

  3.類執行:字節碼被裝入內存,進入JVM虛擬機,被解釋器解釋執行

 

 

1、程序計數器(Program Counter Register)

程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到了第幾行,可以理解爲是當前線程的行號指示器。字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。

  每個程序計數器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數器)的。

  如果程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值爲Undefined,由於程序計數器只是記錄當前指令地址,所以不存在內存溢出的情況,因此,程序計數器也是所有JVM內存區域中唯一一個沒有定義OutOfMemoryError的區域。

 

 

 

2、虛擬機棧(JVM Stack)

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

  局部變量表中存儲着方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會佔用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命週期內都不會改變。

  虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出 StatckOverFlowError(棧溢出);不過多數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。

  每個線程對應着一個虛擬機棧,因此虛擬機棧也是線程私有的。

 

3、本地方法棧(Native Method Statck)

本地方法棧在作用,運行機制,異常類型等方面都與虛擬機棧相同,唯一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。

與虛擬機棧一樣,本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出 StackOverflowError OutOfMemoryError 異常。

  本地方法棧也是線程私有的。

 

4、堆區(Heap)

堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由所有線程共享,在虛擬機啓動時創建。堆區的存在是爲了存儲對象實例,原則上講,所有的對象都在堆區上分配內存(不過現代技術裏,也不是這麼絕對的,也有棧上直接分配的)。

  Java堆既可以被實現成固定大小的,也可以是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現的(通過參數-Xmx和-Xms設定)。如果在Java堆中沒有內存完成實例分配,並且堆也無法再擴展時,Java虛擬機將會拋出 OutOfMemoryError 異常。

 

5、方法區(Method Area)

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

在Java虛擬機規範中,將方法區作爲堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機制分爲3個代:新生代,老年代,永久代,這些作者將方法區定義爲“永久代”,這是因爲,對於之前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。本文中,由於筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。

方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

  方法區在物理上也不需要是連續的,可以選擇固定大小或可擴展大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱爲永久代的原因之一(HotSpot),但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。

 如果方法區無法滿足新的內存分配需求時,將拋出OutOfMemoryError異常。在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。

但是考慮到HotSpot未來的發展,在JDK 6的時候HotSpot開發團隊就有放棄永久代,逐步改爲採用本地內存(Native Memory)來實現方法區的計劃了[插圖],到了JDK 7的HotSpot,已經把原本放在永久代的字符串常量池、靜態變量等移出,而到了JDK 8,終於完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Meta-space)來代替,把JDK 7中永久代還剩餘的內容(主要是類型信息)全部移到元空間中。

 


 

6、運行時常量池:

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table)用於放編譯期生成的各種字面量與符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。Java虛擬機對於Class文件每一部分(自然也包括常量池)的格式都有嚴格規定,如每一個字節用於存儲哪種數據都必須符合規範上的要求才會被虛擬機認可、加載和執行,但對於運行時常量池,《Java虛擬機規範》並沒有做任何細節的要求,不同提供商實現的虛擬機可以按照自己的需要來實現這個內存區域,不過一般來說,除了保存Class文件中描述的符號引用外,還會把由符號引用翻譯出來的直接引用也存儲在運行時常量池中。

運行時常量池相對於Class文件常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說,並非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

 

7、直接內存(Direct Memory)

直接內存並不是JVM管理的內存,可以這樣理解,直接內存,就是JVM以外的機器內存。顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括物理內存、SWAP分區或者分頁文件)大小以及處理器尋址空間的限制,一般服務器管理員配置虛擬機參數時,會根據實際內存去設置-Xmx等參數信息,但經常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

比如,你有4G的內存,JVM佔用了1G,則其餘的3G就是直接內存,JDK中有一種基於通道(Channel)和緩衝區(Buffer)的內存分配方式,將由C語言實現的native函數庫分配在直接內存中,用存儲在JVM堆中的DirectByteBuffer來引用。由於直接內存收到本機器內存的限制,所以也可能出現OutOfMemoryError的異常。

 

 

後面的文章中將會對上面的5大部分進行進一步的學習和分享。

 

參考自《JVM高級特性與最佳實踐(第3版)》

 

 

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