JVM學習之 內存結構

一、引言

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方法 來釋放直接內存。
Tags: