1. 程式人生 > >深入理解JAVA虛擬機器學習筆記(一)JVM記憶體模型

深入理解JAVA虛擬機器學習筆記(一)JVM記憶體模型

一、JVM記憶體模型概述

JVM記憶體模型其實也挺簡單的,這裡先提2個知識點:

1、組成:java堆,java棧(即虛擬機器棧),本地方法棧,方法區和程式計數器。

2、是否共享:其中方法區和堆區是執行緒共享的,虛擬機器棧,本地方法棧和程式計數器是執行緒私有的,也稱執行緒隔離的,每個區域儲存不同的內容。這2個知識點必須牢記,是掌握JVM記憶體模型的基礎。

 二、程式計數器

 JVM中的程式計數器是一塊很小的記憶體區域,但是這塊記憶體區域挺有意思的。主要特性有3個:

1、儲存內容:對於java普通方法(即沒用native關鍵字修飾的方法),儲存的是執行過程中當前指令的地址,而對於native方法,這裡是空的(undefined),為啥呢?因為呼叫本地方法的時候可能已經超出了JVM虛擬機器的記憶體地址了。

2、執行緒私有的:為什麼程式計數器是執行緒私有的?根據儲存內容也好理解,假如是執行緒共享的,那多個執行緒執行的時候,都不知道自己當前執行緒執行的地址是哪個了,有的執行緒快,有的執行緒慢,快的執行完就進入下一步,等慢的執行緒執行完回來發現自己的地址都變了,豈不亂套?

3、是JVM中唯一不會報記憶體溢位(OutOfMemoryError)的區域。

三、虛擬機器棧

虛擬機器棧主要儲存的是一個個棧幀,每個棧幀中儲存的是區域性變量表,運算元棧,動態連結和方法出口資訊等。其中區域性變量表中儲存的是方法中定義的一些區域性變數,物件的引用,引數,和方法的返回地址等。區域性變量表所佔用的空間大小在編譯期就能確定,在方法執行的時候,並不會改變區域性變量表的空間大小,這結合區域性變量表儲存的內容就很好理解。運算元棧可以理解為對當前操作的資料入出棧,對於64位長度的long和double型別,每個運算元佔用2個字寬(slot),其他型別的運算元佔用一個字寬(slot)。每個方法呼叫時都會建立一個棧幀,執行的過程對應的就是一個棧幀在虛擬機器棧中從入棧到出棧的過程。有關棧幀的內容可以參考一個網友寫的一篇部落格:

https://blog.csdn.net/xtayfjpk/article/details/41924283,講的很好很詳細。這裡放個棧幀的圖,看了一目瞭然。

關於虛擬機器棧記憶體溢位有2種情況:

1、執行緒請求的棧深度 超過了虛擬機器允許的深度,會丟擲StackOverflowError,所以當我們在程式碼中看到這個異常時,就應該想到可能是虛擬機器棧出了問題。

2、如果虛擬機器棧可以動態擴充套件(當前大部分JVM都可以動態擴充套件,不過JVM也允許固定長度的虛擬機器棧),當擴充套件時無法申請到足夠的記憶體時,會丟擲OutOfMemoryError異常。

 四、本地方法棧

這塊知識點比較簡單,本地方法棧和虛擬機器棧的功能類似,只不過是為JVM呼叫native方法時服務的,而且JVM對本地方法使用的語言(比如Java呼叫C語言實現的功能,就需要定義native方法來實現)、使用方式和資料結構都沒有強制規定,因此不同的虛擬機器可以自由實現。而且HotSpot虛擬機器直接把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧類似,本地方法棧也會丟擲StackOverflowError和OutOfMemoryError。

五、方法區

方法區是一個比較重要的區域,java虛擬機器規範中把方法區描述為堆的一個邏輯部分,但是為了和Heap(堆區)對應,也稱Non-Heap(非堆區)。主要儲存的是靜態變數,常量(包括執行時常量),類的載入資訊和java編譯後的程式碼。這部分空間不需要連續,可以選擇固定大小和可擴充套件,通常在這部分是沒有GC的,因為GC回收的都是些靜態變數,常量和類的載入資訊,這些物件回收效果通常不盡人意,因此可以選擇不實現垃圾回收。這塊區域也稱為持久代,當這塊記憶體不足時,也會報OutOfMemoryError異常。

六、堆區

Java堆區是JVM記憶體中最胖的一塊區域,因為這裡儲存的都是物件的例項和陣列物件。這塊區域是執行緒共享的,在JVM啟動時就會建立,想想如果這麼大的空間是執行緒私有的,那記憶體不得爆掉嗎?按照java虛擬機器規範,堆區的內容可以物理上不連續,只要邏輯上連續即可,在實現時可以是固定大小的,也可以是可擴充套件的,而且通常都是可擴充套件的,我們常用的記憶體引數-Xms和-Xmx就是用來調節堆大小的。java堆區按生命週期不同,分為新生代和老年代。新生代又可以細分為Eden和Survivor區,而Survivor又可以細分為Survivor1和Survivor2,這兩者通常只使用其中一塊,另一塊用來GC時保留存活的物件。大部分的new出來的物件都是存放在Eden區,如果是大物件,比如一個很大的陣列或者List物件,可以通過JVM引數-XX:PretenureSizeThreshold將超過指定大小的物件直接存入到老年代,需要注意的是,寫程式時應該儘量避免朝生夕死的大物件進入老年代,因為相比年輕代的GC,老年代GC的成本更大。Eden和Survivor的預設大小比值的8:1:1,新生代預設的GC演算法是複製演算法。老年代的預設GC演算法是標記整理法。關於這2種GC演算法,會在下篇部落格講解。

當堆中沒有足夠記憶體時,會丟擲OutOfMemoryError異常。關於堆區的記憶體模型,可以參考下面的圖片: