天天看點

深入了解java虛拟機—— java虛拟機記憶體結構虛拟機的記憶體模型架構解析

深入了解java虛拟機—— 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的作用:

深入了解java虛拟機—— java虛拟機記憶體結構虛拟機的記憶體模型架構解析
  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虛拟機—— 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版)》

繼續閱讀