1. 程式人生 > >深入JVM記憶體區域管理,值得你收藏

深入JVM記憶體區域管理,值得你收藏

JDK和JRE和JVM的關係

JDK(Java Development Kit)是程式開發者用來來編譯、除錯java程式用的開發工具包

JRE(JavaRuntimeEnvironment,Java執行環境),也就是Java平臺。所有的Java 程式都要在JRE下才能執行。普通使用者只需要執行已開發好的java程式,安裝JRE即可

JVM(JavaVirtualMachine,Java虛擬機器)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統

JVM記憶體區域

本文的講解都從這個圖一一開始,你腦海裡先試著回憶一下這個幾個區域的概念,是獨享的還是共享的?每個區域都儲存了什麼?哪些區域會被垃圾回收?哪些區域會丟擲OOM?哪些區域會丟擲SOF?如何避免

什麼是JVM執行時資料區域?

Java虛擬機器定義了在程式執行期間使用的各種執行時資料區域。其中一些資料區域是在Java虛擬機器啟動時建立的,僅在Java虛擬機器退出時才被銷燬。其他資料區域是每個執行緒的。建立執行緒時建立每個執行緒的資料區域,並在執行緒退出時銷燬每個資料區域。

堆記憶體

堆記憶體中儲存的是所有類例項和陣列的記憶體,在虛擬機器啟動時建立,虛擬機器結束時銷燬,歸還給作業系統,堆記憶體中物件的銷燬都JVM自行管理(垃圾收集器),當程式建立物件的越來越多時並且這些物件都無法被回收時,這個區域會丟擲OOM異常,並且堆記憶體是所有執行緒共享的,所以當多個執行緒操作堆記憶體的資料時會有併發問題,要加鎖。

棧記憶體

棧分為虛擬機器棧和本地方法棧,首先棧是執行緒安全的,棧記憶體隨執行緒建立而建立,隨執行緒銷燬而銷燬,棧記憶體是不需要垃圾回收器進行回收的。執行緒棧的大小可以是在虛擬機器啟動時指定固定大小,也可以是自行計算動態擴容的。當指定大小時,執行緒棧的記憶體隨著使用而不足時JVM丟擲StackOverFlowError,當不指定大小時,執行緒棧動態擴容時如果沒有足夠的記憶體不足,JVM將會丟擲OOM錯誤。

虛擬機器棧描述的是Java方法執行的記憶體模型,每個方法在執行時會建立一個棧幀用於儲存放法區域性變量表,運算元棧,動態連結,出口資訊,如下圖,整個棧幀是先入後出。

區域性變量表存放了編譯器可知的各種基本資料型別,物件引用(不包含成員變數)每個區域性變量表佔用32位(4個位元組),所以long和double會佔用兩個區域性變量表,其它型別佔用一個,哪怕byte雖然只有8位,也佔用一個區域性變量表,區域性變量表所需的記憶體在編譯期就已經確定了也就是進入這個方法時就已經確定了,執行期間不會更改.

運算元棧則儲存方法內一些進行了運算操作後的結果.

動態連結,在方法內呼叫介面,通過字面量連結到具體的實現類,實現Java的動態特性.

出口地址(返回地址),return或者發生Exception等。

本地方法棧虛擬機器棧相似,都是執行緒私有的,安全的,區別就是虛擬機器為虛擬機器棧執行Java服務(位元組碼服務),而本地方法棧為虛擬機器使用到的Native方法服務,本地方法棧中使用的語言,使用方式,資料結構沒有強制要求。

程式計數器

Java程式是多執行緒執行的,當一個執行緒執行位元組碼時,突然CPU切換到另一個執行緒,那麼上一個執行緒執行的上下文資訊怎麼儲存呢?等到下次再切換到這個執行緒,從哪裡開始執行呢?這些資訊都需要線上程切換時記錄,這就是程式計數器的職責,是每個執行緒私有的,執行緒安全的,因執行緒建立而建立,因執行緒銷燬而銷燬,程式計數器其實就是一小塊記憶體。

程式計數器指向當前執行緒所執行的位元組碼所在的行號,記錄著當前程式執行到哪了位元組碼直譯器的工作就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支,迴圈,跳轉,異常處理,執行緒回覆等都需要依賴這個計數器來完成

如果一個執行緒執行一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是一個本地方法,這個計數器的值則為undefine,此記憶體區域是唯一一個在Java的虛擬機器規範中沒有規定任何OutOfMemoryError異常情況的區域

元資料區

預設情況下,類元資料只受可用的本地記憶體限制。新引數(MaxMetaspaceSize)用於限制本地記憶體分配給類元資料的大小。如果沒有指定這個引數,元空間會在執行時根據需要動態調整,

這個區域也是會發生GC的,垃圾回收將在元資料使用達到“MaxMetaspaceSize”引數的設定值時進行,適時地監控和調整元空間對於減小垃圾回收頻率和減少延時是很有必要的。如果的元空間持續的發生GC說明可能存在類、類載入器導致的記憶體洩漏或是大小設定不合適,如果這個空間使用達到了MaxMetaspaceSize,但GC無法回收(所有的類資訊都是有用的,所以無法回收),也會發生OOM錯誤。

String常量池已經從方法區(jdk8以前的叫法)中的執行時常量池分離到堆中了,不在元資料中。

Metaspace由兩部分組成:Klass MetaSpace 和 NoKlass MetaSpace,Klass代表的是
class檔案在jvm中執行時的資料結構,NoKlass專門用來儲存Klass相關的其它資料,比如Method和ConstantPool。

回答剛開始的問題

用一段程式碼分析JVM記憶體的儲存

new Thread(new Runnable() {
    @Override
    public void run() {
        test();
    }
    public void test(){
        Object obj = new Object();
    }
}).start();

上面這段程式碼很簡單,啟動了一個執行緒,執行緒的run方法中呼叫了test方法,test方法中建立一個Objet物件,一起來看一下這段程式碼涉及的JVM記憶體哪些區域,分別儲存了什麼。

首先建立了一個執行緒,那麼這個執行緒對應的私有的虛擬機器棧記憶體肯定被分配,這個執行緒的程式碼執行中對應的程式計數器記憶體肯定被分配,因為沒有涉及到本地方法,所有本地棧記憶體不會分配,而且虛擬機器棧記憶體是在編譯器就確定的。

Test方法執行時,建立一個Object物件,我們知道obj是一個引用(reference)型別,所以obj儲存在Java棧的本地變量表中,而在Java堆中會儲存該引用的例項化物件,Java堆中還必須包含能查詢到此物件型別資料的地址資訊(如物件型別、父類、實現的介面,方法等)這些型別資料則儲存在元資料區域中。一般物件引用到物件例項和物件型別指向有兩種方法,一種是控制代碼池方式,一種是直接指標方式。這兩種物件的訪問方式各有優勢,使用控制代碼訪問方式的最大好處就是reference中存放的是穩定的控制代碼地址,在物件的移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而reference本身不需要修改。使用直接指標訪問方式的最大好處是速度快,它節省了一次指標定位的時間開銷。目前Java預設使用的HotSpot虛擬機器採用的便是是第二種方式進行物件訪問的,下面用兩張圖來表述一下這兩種方式。

這張圖是控制代碼池方式

這張圖是直接指標方式

關於基本資料型別和引用型別的分配

基本資料型別包括 int short long bolean等,引用型別就是我們常見的物件,那麼這兩種資料型別記憶體中是怎麼分配的呢?這個得區別看待,我們根據下面程式碼來分析

class  Dog {
    private int age;
}
class Test{
    public void test(){
        Dog dog = new Dog();
        dog.age = 2;
        int age = 1;
        Integer age = new Integer(3);
    }
}

在Test類中的test方法中,我們建立了一個Dog物件,這個物件例項是分配在堆上的,dog這個引用是在棧上的,dog中的age在哪裡呢?因為Dog物件例項是在堆上的,所有他的成員變數也是在堆上的。 int age這個變數是棧上的,因為它是區域性變數,並且是基本資料型別,Integer age例項是在堆上的,引用是在棧上的,根據這個例子,可以總結下面兩條基本黃金法則

  1. 引用型別總是被分配到“堆”上。
  2. 值型別總是分配到它宣告的地方:    a. 作為引用型別的成員變數分配到“堆”上    b. 作為方法的區域性變數時分配到“棧”上

總結

本文詳細介紹了JVM記憶體區域的各個情況,也就是JVM記憶體模型,也解答了一些常見的面試題和記憶體分配相關的一些問題,希望能夠幫助到讀者更好的瞭解到JVM,可能會有人有些疑問,為什麼不說堆記憶體的分代(年輕代,年老代)問題呢?我認為這個屬於JVM垃圾回收的方位,分代思想只是解決垃圾收回問題的一種方法,同理,Java8中G1的region也是一樣,都是為了解決垃圾回收效率和效能問題,會放在JVM垃圾回收一文來說。