淺入淺出JVM - 類加載及運行時數據區

一 分區及流程圖

ClassLoader:

作用:負責加載class文件,class文件在文件開頭有特定的文字標識(cafe babe),將class文件字節碼內容加載到內存中,並將這些內容轉換成方法中的運行時數據結構,並且ClassLoader只負責class文件的加載至於它是否可以運行,則由Execution Engine決定。

ClassLoader的雙親委派機制:

作用: 主要是能夠保障Java平臺的安全性, 防止內存中出現多份同樣的字節碼。

實現:

  • 當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

  • 當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

  • 如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;

  • 若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。

沙箱安全機制:

沙箱機制是由基於雙親委派機制上採取的一種JVM的自我保護機制, 假設你要寫一個java.lang.String 的類, 由於雙親委派機制的原理, 此請求會先交給Bootstrap試圖進行加載, 但是Bootstrap在加載類時首先通過包和類名查找rt.jar中有沒有該類, 有則優先加載rt.jar包中的類, 因此就保證了java的運行機制不會被破壞.。

程序計數器(PC寄存器):

  1. PC寄存器( PC register ):每個線程啓動的時候,都會創建一個PC(Program Counter,程序計數器)寄存器。PC寄存器裏保存有當前正在執行的JVM指令的地址。 每一個線程都有它自己的PC寄存器,也是該線程啓動時創建的。保存下一條將要執行的指令地址的寄存器是 :PC寄存器。PC寄存器的內容總是指向下一條將被執行指令的地址,這裏的地址可以是一個本地指針,也可以是在方法區中相對應於該方法起始指令的偏移量。

  2. 每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。

  3. 這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。

  4. 如果執行的是一個Native方法,那這個計數器是空的。

方法區:

作用:供各線程共享的運行時內存區域。它存儲了每一個類的結構信息,例如運行時常量池、字段和方法數據、構造方法和普通方法的字節碼內容。這是規範,但在不同虛擬機裏面實現是不一樣的,最典型的就是永久代(PermGen Space)和元空間(Metaspace)。但是,實例變量存在堆內存中,與方法區無關

棧區:

作用:棧管運行,堆管存儲。棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命週期跟隨線程的生命週期。線程結束時棧內存也就釋放,對於棧來說不存在垃圾回收問題,是線程私有的。8種基本數據類型的變量+對象的引用變量+實例方法都是在棧內存中分配

棧存儲:棧幀中主要保存三類數據:

  • 本地變量(Local variables):輸入參數和輸出參數以及方法內的變量。

  • 棧操作(OPerate Stack):記錄入棧、出棧的操作。

  • 棧幀數據(Frame Data):包括類文件、方法等。

棧運行原理:棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法和運行期數據的數據集;

當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,

A方法又調用了B方法,於是產生棧幀F2也被壓入棧,

B方法又調用了C方法,於是產生棧幀F3也被壓入棧,

。。。。。。

執行完畢後按照棧FILO的特性依次彈出F3、F2、F1棧幀。

每個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完畢的過程就對應着一個棧幀從入棧到出棧的過程。棧的大小和具體的JVM實現有關,通常在256K~756K之間,約等於1Mb左右。

棧、堆及方法區的交互關係:

Hotspot是使用指針的方式來訪問對象:

Java堆中會存放訪問類元數據的地址, reference存儲的就是對象的地址。

堆區:

一個JVM實例只存在一個堆內存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行,堆內存分爲三部分:

  • Young Generation Space 新生代 Young/New

  • Tenure generation space 老年代 Old/ Tenure

  • Permanent Space 永久代/元空間 Perm

新生區:

新生區是類的誕生、成長、消亡的區域,一個類在這裏產生,應用,最後被垃圾回收器收集,結束生命。新生區又分爲兩部分: 伊甸區(Eden space)和倖存者區(Survivor pace) ,所有的類都是在伊甸區被new出來的。倖存區有兩個: 0區(Survivor 0 space / From Survivor)和1區(Survivor 1 space / To Survivor)。當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他對象所引用的對象進行銷燬。然後將伊甸園中的剩餘對象移動到倖存 0區。若倖存 0區也滿了,再對該區進行垃圾回收,然後移動到 1 區。那如果1 區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生MajorGC(FullGC),進行養老區的內存清理。若養老區執行了Full GC之後發現依然無法進行對象的保存,就會產生OOM異常“OutOfMemoryError”。

如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。原因有二: (1)Java虛擬機的堆內存設置不夠,可以通過參數-Xms、-Xmx來調整。 (2)代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)。

MinorGC的過程(複製->清空->互換:

  1. eden、SurvivorFrom 複製到 SurvivorTo,年齡+1 首先,當Eden區滿的時候會觸發第一次GC,把還活着的對象拷貝到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的對象,則直接複製到To區域(如果有對象的年齡已經達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1。

  2. 清空 eden、SurvivorFrom 然後,清空Eden和SurvivorFrom中的對象,也即複製之後有交換,誰空誰是to。

  3. SurvivorTo和 SurvivorFrom 互換 最後,SurvivorTo和SurvivorFrom互換,原SurvivorTo成爲下一次GC時的SurvivorFrom區。部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終如果還是存活,就存入到老年代。

實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯後的代碼等等,雖然JVM規範將方法區描述爲堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

對於HotSpot虛擬機,很多開發者習慣將方法區稱之爲“永久代(Parmanent Gen)” ,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區(相當於是一個接口interface)的一個實現,jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。

堆區參數調優:

在Java8中,永久代已經被移除,被一個稱爲元空間的區域所取代。元空間的本質和永久代類似。

元空間與永久代之間最大的區別在於: 永久代使用的JVM的堆內存,但是java8以後的元空間並不在虛擬機中而是使用本機物理內存。

因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。

public static void main(String[] args){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虛擬機試圖使用的最大內存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虛擬機中的內存總量。
System.out.println("MAX_MEMORY = " + maxMemory + "(字節)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字節)、" + (totalMemory / (double)1024 / 1024) + "MB");
}

VM參數: -Xms1024m -Xmx1024m -XX:+PrintGCDetails

 

如果你遇到了其他的問題或者你也和我一樣對技術充滿熱情, 歡迎隨時與我交流! wechat: s13037657871
 

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