JVM結構-記憶體結構(執行時資料區)

weixin_33831196發表於2017-09-03

我們知道JVM記憶體結構也就是java程式時執行區,所以在瞭解之前首先對其思考:

  • JVM記憶體結構都包含哪幾部分,都是如何劃分的?
  • 每部分都是儲存的什麼資料?
  • 真正執行時,一個物件建立的時候,是怎麼分配的?

下面我們就以上面3個問題為引子,來了解JVM的記憶體結構。

一.JVM的記憶體結構

如下圖所示:

5307705-c56589034c79a6fb.png
jvm記憶體結構.png

可以看到,執行時資料區主要包含:方法區、堆、java棧、本地方法棧、程式計數器。其中方法區和堆是所有執行緒共享的,java棧、本地方法棧、程式計數器是執行緒私有的

二、JVM結構分拆

1.程式計數器(Program Counter Register)

    程式技術器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的行號指示器。在任意時刻,一條 Java 虛擬機器執行緒只會執行一個方法的程式碼,這個正在被執行緒執行的方法稱為該執行緒的當前方法(Current Method)。如果這個方法不是 native 的,那 PC 暫存器就儲存 Java 虛擬機器正在執行的位元組碼指令的地址,如果該方法是 native 的,那 PC 暫存器的值是 undefined。PC 暫存器的容量至少應當能儲存一個 returnAddress 型別的資料或者一個與平臺相關的本地指標的值。此記憶體是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

2.java虛擬機器棧

    與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

我們平時所說的基本變數在棧中分配,就是說的java棧,準確來說是說的區域性變數表。

對於Java棧會出現一下2種異常情況:

  • 如果執行緒請求的棧的深度大於虛擬機器所允許的深度,將會丟擲StackOverflowError異常;
  • 如果 Java 虛擬機器棧可以動態擴充套件,並且擴充套件的動作已經嘗試過,但是目前無法申請到足夠的記憶體去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那 Java 虛擬機器將會丟擲一個 OutOfMemoryError 異常。

3.本地方法棧

    本地方法棧和虛擬機器棧所發揮的作用非常相似,它們之間的區別是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,本地方法棧為虛擬機器使用到的Native方法(c或c++)服務。也會丟擲StackOverflowError和OutOfMemoryError異常

4.堆(Heap)

    在 Java 虛擬機器中,堆(Heap)是可供各條執行緒共享的執行時記憶體區域,也是供所有類例項和陣列物件分配記憶體的區域。
    Java堆是垃圾收集器管理的主要區域,所有又稱為"GC堆"(Garbage Collected Heap)。

  • Java堆的內部結構如下:
  • Java堆由新生代(YoungGeneration)和老年代(OldGeneration)構成。年輕代又分為3部分:Eden(伊甸)區、From Survivor(倖存)區、To Survivor區,預設情況下年輕代按照8:1:1的比例來分配(-XX:SurvivorRatio)。
5307705-89c89e3c7acac82e.png
java 堆.png

JVM控制引數:

  • -Xms:JVM初始時堆記憶體大小
  • -Xmx:JVM最大可用的堆記憶體大小

JVM也是一個軟體,也必須要獲取本機的實體記憶體,然後JVM會負責管理向作業系統申請到的記憶體資源。JVM啟動的時候會向作業系統申請 -Xms 設定的記憶體,JVM啟動後執行一段時間,如果發現記憶體空間不足,會再次向作業系統申請記憶體。JVM能夠獲取到的最大堆記憶體是-Xmx設定的值。

  • -XX:MaxNewSize設定新生代最大空間大小。
  • -XX:NewSize設定新生代最小空間大小。

沒有直接設定老年代大小的引數,但可以計算出來:
老年代大小=堆記憶體大小-新生代大小

  • -XX:MaxPermSize設定永久代最大空間大小。
  • -XX:PermSize設定永久代最小空間大小。
  • -XX:SurvivorRatio:設定新生代中1個Eden區與1個Survivor區的大小比值,預設設定8。如新生代的大小為100M,則Eden區大小為80M,2個Survivor區大小分別為10M。
  • -Xss設定每個執行緒的堆疊大小。
5307705-fad8050feaebb82a.png
jvm 引數.png
  • 如果實際所需的堆超過了自動記憶體管理系統能提供的最大容量,那 Java 虛擬機器將會丟擲一個OutOfMemoryError 異常。

5.方法區

    方法區用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即使編譯器編譯或的程式碼等資料,所以它是執行緒共享的。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

a.類及其父類的全限定名(java.lang.Object沒有父類)
b.類的型別(Class or Interface)
c.訪問修飾符(public, abstract, final)
d.實現的介面的全限定名的列表
e.常量池
f.欄位資訊
g.方法資訊
h.靜態變數
i.ClassLoader引用
j.Class引用

    對於習慣在HotSpot虛擬機器上開發和部署程式的開發者來說,很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為HotSpot虛擬機器的設計團隊選擇把GC分代收集擴充套件至方法區,或者說使用永久代來實現方法區而已

    Java虛擬機器規範對這個區域的限制非常寬鬆,除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充套件外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是型別的解除安裝,條件相當苛刻,但是這部分割槽域的回收確實是有必要的。

根據Java虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

永久代變更:

  • jdk1.7 HotSpot已經字串常量池從“永久代”移除到堆中,String.intern詳解——參見深入理解String#intern
  • jdk1.8中沒有了永久代,替而代之的是:將方法區直接放在一個與堆不相連的本地記憶體區域,這個區域被叫做元空間(metaspace)——java8元空間

6.執行時常量池

    執行時常量池是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。

字面量:直接給值,不宣告變數儲存。例:int a = 3; 其中a是變數,3是字面量。
符號引用:在java中,一個java類將會編譯成一個class檔案。在編譯時,java類並不知道引用類的實際記憶體地址,因此只能使用符號引用來(還不知道類的具體地址,使用能找到該類的一個類全限定名錶示)代替。比如org.simple.People類引用org.simple.Tool類,在編譯時People類並不知道Tool類的實際記憶體地址,因此只能使用符號org.simple.Tool(假設)來表示Tool類的地址。而在類裝載器裝載People類時,此時可以通過虛擬機器獲取Tool類 的實際記憶體地址,因此便可以既將符號org.simple.Tool替換為Tool類的實際記憶體地址,即直接引用地址。

根據Java虛擬機器規範的規定,當常量池無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

7.直接(堆外)記憶體

    直接記憶體並不是虛擬機器執行資料去的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁的使用,也會導致OutOfMemoryError異常。
    在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料。——java堆外記憶體詳解

三、物件訪問定位

目前主流的訪問物件的方式是使用控制程式碼和直接指標。

5307705-b363fec4266b4aad.png
控制程式碼訪問物件.png
5307705-61432c59c08a3f85.png
直接指標訪問物件.png

相關文章