JVM學習之 記憶體結構

2022-08-06 21:01:23

一、引言

1.什麼是JVM?

  1. 定義:Java Virtual Machine - java 程式的執行環境(java 二進位制位元組碼的執行環境)
  2. 好處:
    • 一次編寫,到處執行
    • 自動記憶體管理, 垃圾回收功能
    • 陣列下標越界檢查
    • 多型
  3. 比較jvm、jre、jdk

2.學習JVM有什麼用

  • 理解底層的實現原理
  • 中高階程式設計師的必備技能

3.常見的JVM

4.學習路線

二、記憶體結構

1. 程式計數器

1.1 定義

Program Counter Register 程式計數器(暫存器)
在物理上:位於暫存器
作用:是記住下一條jvm指令的執行地址
特點:

  • 是執行緒私有的
  • 不會存在記憶體溢位

1.2作用

0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
  • 直譯器會解釋指令為機器碼交給 cpu 執行,程式計數器會記錄下一條指令的地址行號,這樣下一次直譯器會從程式計數器拿到指令然後進行解釋執行。
  • 多執行緒的環境下,如果兩個執行緒發生了上下文切換,那麼程式計數器會記錄執行緒下一行指令的地址行號,以便於接著往下執行。

2. 虛擬機器器棧

2.1定義

Java Virtual Machine Stacks (Java 虛擬機器器棧)

  • 每個執行緒執行時所需要的記憶體,稱為虛擬機器器棧
  • 每個棧由多個棧幀(Frame)組成,對應著每次方法呼叫時所佔用的記憶體
  • 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法
  • 問題辨析
  1. 垃圾回收是否涉及棧記憶體?
    棧記憶體並不涉及垃圾回收,棧記憶體的產生就是方法一次一次呼叫產生的棧幀記憶體,而棧幀記憶體在每次方法被呼叫後都會被彈出棧,自動就被回收掉,不需要垃圾回收。來管理

  2. 棧記憶體分配越大越好嗎?
    不是,線上程不多的情況下,棧記憶體分配大在遞迴時能提高執行速度,但他會影響執行緒的數目,從而影響到整個系統的執行速度

  3. 方法內的區域性變數是否執行緒安全?
    如果方法內區域性變數沒有逃離方法的作用存取,它是執行緒安全的 如果是區域性變數參照了物件,並逃離方法的作用範圍,需要考慮執行緒安全,

    如果是共用的需要考慮執行緒安全,如果是私有的不用考慮執行緒安全

2.2棧記憶體溢位

  • 棧幀過多導致棧記憶體溢位 ---->一般遞迴的時候容易出現
  • 棧幀記憶體過大導致棧記憶體溢位 --->不易出現
  • StackOverflowError 棧記憶體溢位異常
  • @JsonIgnore

2.3執行緒執行診斷

案例1:cpu佔用過多
定位

  • 用top定位哪個程序對cpu的佔用過高
  • ps H -eo pid,tid,%cpu | grep 程序id (用ps命令進一步定位是哪個執行緒引起的cpu佔用過高)
  • jstack 程序id
    可以根據執行緒id 找到有問題的執行緒,進一步定位到問題程式碼的原始碼行號

3. 本地方法棧

一些帶有 native 關鍵字的方法就是需要 JAVA 去呼叫原生的C或者C++方法,因為 JAVA 有時候沒法直接和作業系統底層互動,所以需要用到本地方法棧,服務於帶 native 關鍵字的方法。

為原生的方法提供一個執行的空間

4. 堆

4.1定義

Heap 堆

  • 通過 new 關鍵字,建立物件都會使用堆記憶體
    特點
  • 它是執行緒共用的,堆中物件都需要考慮執行緒安全的問題
  • 有垃圾回收機制

4.2堆記憶體溢位

  1. jps 工具
    檢視當前系統中有哪些 java 程序
  2. jmap 工具
    檢視堆記憶體佔用情況 jmap - heap 程序id
  3. jconsole 工具
    圖形介面的,多功能的監測工具,可以連續監測
  4. jvisualvm 工具

5. 方法區

5.1方法區


Java 虛擬機器器有一個在所有 Java 虛擬機器器執行緒之間共用的方法區。方法區類似於傳統語言的編譯程式碼的儲存區,或者類似於作業系統程序中的「文字」段。它儲存每個類的結構,例如執行時常數池、欄位和方法資料,以及方法和建構函式的程式碼,包括類和範例初始化以及介面初始化中使用 的特殊方法。

方法區是在虛擬機器器啟動時建立的。儘管方法區在邏輯上是堆的一部分,但簡單的實現可能會選擇不進行垃圾收集或壓縮它。本規範不要求方法區域的位置或用於管理已編譯程式碼的策略。方法區域可以是固定大小,也可以根據計算需要擴大,如果不需要更大的方法區域,可以縮小。方法區的記憶體不需要是連續的。

Java 虛擬機器器實現可以為程式設計師或使用者提供對方法區域初始大小的控制,以及在方法區域大小可變的情況下,對最大和最小方法區域大小的控制。

以下異常情況與方法區相關:

如果方法區域中的記憶體無法滿足分配請求,Java 虛擬機器器將丟擲一個OutOfMemoryError.
JVM規範-方法區定義

5.2組成

5.3方法區記憶體溢位

  • 1.8 以前會導致永久代記憶體溢位
    演示永久代記憶體溢位 java.lang.OutOfMemoryError: PermGen space
    -XX:MaxPermSize=8m

  • 1.8 之後會導致元空間記憶體溢位
    演示元空間記憶體溢位 java.lang.OutOfMemoryError: Metaspace
    -XX:MaxMetaspaceSize=8m
    場景:

      spring
      mybatis

5.4 執行時常數池

// 二進位制位元組碼(類基本資訊,常數池,類方法定義,包含了虛擬機器器指令)
public class Test {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

然後使用 javap -v Test.class 命令反編譯檢視結果:

每條指令都會對應常數池表中一個地址,常數池表中的地址可能對應著一個類名、方法名、引數型別等資訊。

  • 常數池,就是一張表,虛擬機器器指令根據這張常數表找到要執行的類名、方法名、引數型別、字面量
    等資訊
  • 執行時常數池,常數池是 *.class 檔案中的,當該類被載入,它的常數池資訊就會放入執行時常數 池,並把裡面的符號地址變為真實地址

5.5 StringTable

面試題:

        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab
        String s4 = s1 + s2;   // new String("ab")
        String s5 = "ab";
        String s6 = s4.intern();

// 問
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true

        String x2 = new String("c") + new String("d"); // new String("cd")
        x2.intern();
        String x1 = "cd";

// 問,如果調換了【最後兩行程式碼】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2);
                // jdk1.6:
        // String x1 = "cd";            x2.intern();
        // x2.intern();  false          String x1 = "cd"; ture

        // jdk1.8:
        // String x1 = "cd";            x2.intern();
        // x2.intern();  false          String x1 = "cd"; ture

練習:

// StringTable [ "a", "b" ,"ab" ]  hashtable 結構,不能擴容
public class Demo1_22 {
    // 常數池中的資訊,都會被載入到執行時常數池中, 這時 a b ab 都是常數池中的符號,還沒有變為 java 字串物件
    // ldc #2 會把 a 符號變為 "a" 字串物件
    // ldc #3 會把 b 符號變為 "b" 字串物件
    // ldc #4 會把 ab 符號變為 "ab" 字串物件

    public static void main(String[] args) {
        String s1 = "a"; // 懶惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在編譯期間的優化,結果已經在編譯期確定為ab
        System.out.println(s3 == s4);//s3是在串池中的,而s4則是在堆中,所有不相等

        System.out.println(s3 == s5);// true
    }
}

使用javap -v Demo1_22.class命令

5.6 StringTable的特性

  • 常數池中的字串僅是符號,第一次用到時才變為物件
  • 利用串池的機制,來避免重複建立字串物件
  • 字串變數拼接的原理是 StringBuilder (1.8)
  • 字串常數拼接的原理是編譯期優化
  • 可以使用 intern 方法,主動將串池中還沒有的字串物件放入串池
    1.8 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串 池中的物件的參照返回
    1.6 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有會把此物件複製一份, 放入串池, 會把串池中的物件返回

5.7 StringTable 位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

5.8 StringTable 垃圾回收

-Xmx10m 指定堆記憶體大小
-XX:+PrintStringTableStatistics 列印字串常數池資訊
-XX:+PrintGCDetails
-verbose:gc 列印 gc 的次數,耗費時間等資訊

演示StingTable垃圾回收:

public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }

5.9 StringTable 效能調優

調整 -XX:StringTableSize=桶個數

考慮將字串物件是否入池

6. 直接記憶體

6.1 定義

Direct Memory

  • 常見於 NIO 操作時,用於資料緩衝區
  • 分配回收成本較高,但讀寫效能高
  • 不受 JVM 記憶體回收管理

檔案讀寫過程(IO):

因為 java 不能直接操作檔案管理,需要切換到核心態,使用本地方法進行操作,然後讀取磁碟檔案,會在系統記憶體中建立一個緩衝區,將資料讀到系統緩衝區, 然後在將系統緩衝區資料,複製到 java 堆記憶體中。缺點是資料儲存了兩份,在系統記憶體中有一份,java 堆中有一份,造成了不必要的複製。

使用了 DirectBuffer 檔案讀取流程:

直接記憶體是作業系統和 Java 程式碼都可以存取的一塊區域,無需將程式碼從系統記憶體複製到 Java 堆記憶體,從而提高了效率。

6.2 分配和回收原理

  • 使用了 Unsafe 物件完成直接記憶體的分配回收,並且回收需要主動呼叫 freeMemory 方法
  • ByteBuffer 的實現類內部,使用了 Cleaner (虛參照)來監測 ByteBuffer 物件,一但ByteBuffer 物件被垃圾回收,那麼就會由 ReferenceHandler 執行緒通過 Cleaner 的 clean 方法呼叫 freeMemory方法 來釋放直接記憶體。