JVM深入理解

Java虛擬機(JVM)

1、JVM的位置

 

2、JVM體系結構

 

3、類載入器

 

 

3.1、類載入器的作用

  • 將class文件位元組碼內容載入到記憶體中,並將這些靜態數據轉換成方法區的運行時數據結構,然後在堆中生成一個代表這個類的 java.lang.Class 對象,作為方法區中類數據的訪問入口

 

3.2、類載入器的分類

  • 引導類載入器:C++編寫的,JVM自帶的類載入器,負責Java平台核心庫,用來裝載核心類庫,該載入器無法直接獲取

  • 擴展類載入器:主要載入%JAVA_HOME%/jre/lib/ext,此路徑下的所有classes目錄以及java.ext.dirs系統變數指定的路徑中類庫

  • 系統類載入器:最常用的載入器,主要負責載入classpath所指定的位置的類或者是jar文檔

  • 用戶自定義類載入器:用戶自定義的類載入器,可載入指定路徑的class文件

 

3.3、類載入步驟分析

//以這段程式碼為例
public class Test03 {
   public static void main(String[] args) {
       AAA aaa = new AAA();
  }
}

class AAA{
   static {
       System.out.println("類AAA的靜態程式碼程式碼塊");
  }

   static int num = 100;

   public AAA() {
       System.out.println("類AAA的構造方法");
  }
}
  • 載入(Loading)

    • 將class文件位元組碼內容載入到記憶體中,並將這些靜態數據轉換成方法區的運行時數據結構,然後生成一個代表這個類的java.lang.Class對象

  • 鏈接(Linking):將Java類的二進位程式碼合併到JVM的運行狀態之中的過程

    • 驗證:確保載入的資訊符合JVM規範,沒有安全方面的問題

    • 準備:正式為類變數(static)分配記憶體並設置類變數默認初始值的階段,這些記憶體都將在方法區中進行分配

    • 解析:虛擬機常量池內的符號引用(常量名)替換成直接引用(地址)的過程

  • 初始化

    • 執行類構造器方法<clinit>()的過程,類構造器方法<clinit>()是由編譯期自動收集類中所有類變數的賦值動作和靜態程式碼塊中的語句合併產生的

    • 當初始化一個類時,如果發現其父類還沒有初始化,則需要先觸發其父類的初始化

    • 虛擬機會保證一個類的<clinit>()方法在多執行緒中會被正確載入和同步

 

3.4、雙親委派機制

  • 概念

    當某個類載入器需要載入某個.class文件時,它首先把這個任務委託給他的上級類載入器,遞歸這個操作,如果上級的類載入器沒有載入,自己才會去載入這個類。

  • 圖解

  • 作用

    • 從一定程度上可以防止危險程式碼的植入,保證程式碼安全,如果有人想替換系統級別的類:String.java。篡改它的實現,但是在這種機制下這些系統的類已經被Bootstrap classLoader載入過了,所以並不會再去載入。

    • 防止重複載入同一個類,通過委託機制,如果級別高的載入器載入過了,就不用再載入一遍。

 

4、本地方法棧

 

5、程式計數器

  • 每個執行緒都有自己私有的的一個程式計數器,一個指針,指向方法區中的方法位元組碼(用來存儲指向下一條指令,也就是即將要執行的指令的地址),在執行引擎讀取下一條指令。

  • 程式計數器所佔記憶體空間非常小,幾乎可以忽略不計。

 

6、棧

 

棧結構解析

  • 局部變數表

 

  • 操作數棧

  • 動態連接

    在Class文件的常量池中存有大量的符號引用,位元組碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用一部分會在類載入階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態連接。

    每個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是為了支援方法調用過程中的動態連接。

  • 返回地址

    • 當一個方法開始執行後,有兩種方式可以結束方法:

      • 正常完成出口:

        當程式遇到返回指令,會將返回值傳遞給上層的方法調用者,一般來說,調用者的PC計數器可以當成返回地址。

      • 異常完成出口:

        當程式執行遇到異常,並且沒有處理或者拋出異常,就會導致方法退出,此時方法沒有返回值,返回地址需要通過異常處理表來確定。

    • 當方法返回時,可能執行的操作:

      • 恢復上層方法的局部變數表和操作數棧

      • 把返回值壓入調用者棧幀的操作數棧

      • 調整棧幀PC計數器的值以執行方法調用後面的一條指令

 

7、方法區

  • jdk8後在堆中的元空間中

  • 被所有執行緒共享的一片區域

  • 存放:靜態的(static)、常量的(final)、類資訊(構造方法、介面定義)、運行時的常量池

 

8、堆

  • 一個JVM只有一個堆記憶體,大小可以調節

  • 類的具體實例、類中常量、變數、方法,保存所有引用類型的真實對象

 

8.1、堆結構

  • 堆結構詳解

    • 新生區

      類誕生、生長、以及可能死亡的地方

      • 伊甸園:所有對象的實例化都是在伊甸園產生的

      • 倖存區:伊甸園中經過垃圾回收後還存活的對象會進入到倖存區

    • 老年區

      新生區中經過垃圾回收後還存活的對象會進入到老年區

    • 永久區

      存儲的是Java運行時的一些環境或類資訊,JVM被關閉後會釋放這個區域的記憶體,不存在垃圾回收

      永久區是邏輯上存在但是物理上不存在的一片區域

      • jdk 1.6 之前:名為永久代,此時常量池在方法區中

      • jdk 1.7:名為永久代,但永久代慢慢退化,此時常量池在堆中

      • jdk 1.8 之後:名為元空間,此時常量池在元空間中

      //查看堆記憶體具體各區域所佔空間大小,本例中虛擬機的總記憶體為2M

      //查看虛擬機的總記憶體,程式碼如下
      long totalMemory = Runtime.getRuntime().totalMemory();
      //輸出結果:虛擬機的總記憶體為:2.0MB
      System.out.println("虛擬機的總記憶體為:" + (totalMemory/(double)1024/1024) + "MB");

      //列印GC詳細資訊
      Heap
      PSYoungGen    total 1536K,
          used 1455K [0x00000000ffd80000, 0x00000000fff80000, 0x0000000100000000)
          eden space 1024K,
              94% used [0x00000000ffd80000,0x00000000ffe71e10,0x00000000ffe80000)
          from space 512K,
              95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
          to   space 512K
              0% used [0x00000000ffe80000,0x00000000ffe80000,0x00000000fff00000)
      ParOldGen    total 512K,
          used 128K [0x00000000ff800000, 0x00000000ff880000, 0x00000000ffd80000)
          object space 512K,
              25% used [0x00000000ff800000,0x00000000ff820000,0x00000000ff880000)
      Metaspace    used 3505K, capacity 4500K, committed 4864K, reserved 1056768K
       class space    used 389K, capacity 392K, committed 512K, reserved 1048576K
                   
      //結果分析
      PSYoungGen(新生代):共1536k,1.5MB
      ParOldGen(老年代):共512k,0.5MB
      這些相加總記憶體已經等於2MB了,所以即使永久區(元空間)使用的有記憶體,但是它邏輯上存在但是物理上不存在

 

8.2、OOM

OutOfMemoryError:記憶體不足錯誤,說明堆記憶體滿了

  • 解決:

    • 使用堆記憶體調優參數調整堆記憶體大小,排查是否還會出錯

    • 若還會出錯,使用JProfiler工具分析記憶體,排查具體出錯位置

 

8.3、堆記憶體調優參數

  • -Xms1m:

    設置初始化記憶體分配大小,默認為記憶體大小的1/64

  • -Xmx8m:

    設置最大分配記憶體,默認為記憶體大小的1/4

  • -XX:+PrintGCDetails:

    列印垃圾回收的詳細資訊

  • -XX:+HeapDumpOnOutOfMemoryError

    Dump記憶體文件

  • -XX:MaxTenuringThreshold(默認15):

    設置倖存區里的對象經過多少次GC會進入老年區

  • ……

 

8.4、使用JProfiler工具分析OOM

  • 首先要設置Dump記憶體文件

    • 配置:-XX:+HeapDumpOnOutOfMemoryError

 

  • 執行程式在控制台找到dump記憶體文件的名字

 

  • 找到dump記憶體文件的所在位置

    雙擊打開

 

      打開之後如下圖所示

 

  • 點擊Biggest Object分析哪個對象的Retained size存在問題

 

  • 點擊Thread Dump查看哪個執行緒在具體哪個位置出現了問題

 

9、GC

  • Garbage Collector

    垃圾回收器

  • 作用區域

    堆中的方法區和新生代,老年代

  • 分類

    • 輕GC(普通GC)

    • 重GC(全局GC)

9.1、常見演算法

  • 引用計數法

    對每個對象都加上一個計數器,對象每被引用一次,計數器都加1,沒被引用的對象計數器減1,垃圾回收器會回收計數器清0的對象。

 

  • 複製演算法

    • 優點:沒有記憶體碎片

    • 缺點:浪費一片記憶體空間

 

  • 標記清除演算法

    • 優點:不需要額外的記憶體空間

    • 缺點:兩次掃描,嚴重浪費時間,會產生記憶體碎片

 

  • 標記壓縮演算法

    • 優點:不會產生記憶體碎片

    • 缺點:要經過三次掃描,時間複雜度高

 

9.2、總結

  • 記憶體效率(時間複雜度):複製演算法 > 標記清除演算法 > 標記壓縮演算法

  • 記憶體整齊度:複製演算法 = 標記壓縮演算法 > 標記清除演算法

  • 記憶體利用率:標記壓縮演算法 = 標記清除演算法 > 複製演算法

  • 分代收集演算法

    • 新生代:複製演算法

    • 老年代:標記清除演算法 + 標記壓縮演算法