JVM學習筆記——記憶體結構篇

JVM學習筆記——記憶體結構篇

在本系列內容中我們會對JVM做一個系統的學習,本片將會介紹JVM的記憶體結構部分

我們會分為以下幾部分進行介紹:

  • JVM整體介紹
  • 程式計數器
  • 虛擬機棧
  • 本地方法棧
  • 方法區
  • 直接記憶體

JVM整體介紹

我們在正式開始學習JVM之前當然需要先簡單認識一下JVM了

JVM簡述

首先我們給出JVM的定義:

  • Java Virtual Machine – java 程式的運行環境(java 二進位位元組碼的運行環境)

JVM的優點:

  • 一次編寫,到處運行
  • 自動記憶體管理,垃圾回收功能
  • 數組下標越界檢查
  • 多態

常見JVM展示

我們下面給出常見的JVM視圖展示:

目前我們所講述的JVM知識基本都是基於HotSpot類型的JVM

JVM總體路線

我們給出JVM的整體框架,而該框架也是我們學習JVM的總體路線:

我們的學習順序如下:

  • JVM記憶體結構
  • GC垃圾回收
  • Java Class
  • ClassLoader
  • JIT Compiler

JVM,JRE,JDK比較

我們順便介紹一個面試常見問題:

  • 請給出JVM,JRE,JDK之間的區別

我們首先採用一張圖進行解釋:

我們來做出簡單介紹:

  • JVM是我們的Java程式最基本的底層架構,我們通過JVM來實現Java源程式碼和作業系統之間的交互
  • JRE在JVM的基礎上添加了我們平時所使用的基礎類庫,包括有Net Framekwork的核心類庫等相關庫
  • JDK在JRE的基礎上又添加了編譯工具,包括有jar打包工具,Java運行工具,Javac編譯工具,Javadoc文檔工具等
  • JavaSE程式在JDK的基礎上又添加了我們常用的開發工具,市面上我們常見的IDEA或者VS等系列工具
  • JavaEE是在 JavaSE 的基礎上構建的,它提供Web 服務,通訊 API等,可以用來實現企業級的面向服務和Web 3.0應用程式。

程式計數器

首先我們先來介紹JVM記憶體結構中的程式計數器

程式計數器簡述

首先我們給出程式計數器的簡單定義:

  • Program Counter Register 程式計數器(暫存器)

然後我們給出程式計數器的主要作用:

  • 程式計數器主要用於記錄下一條jvm指令的執行地址

程式計數器具有以下特點:

  • 程式計數器默認情況下不可能出現記憶體溢出

  • 程式計數器是一塊較小的記憶體空間,它通常採用暫存器代替

  • 程式計數器綁定執行緒,每個執行緒有且只有一個程式計數器,它隨著執行緒創建而創建,隨著執行緒銷毀而銷毀

程式計數器詳細介紹

我們給出一些程式碼來進行簡單介紹:

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

我們下面進行簡單解釋:

  • 首先我們的注釋部分是Java的源程式碼,左側部分是我們的二進位位元組碼即jvm指令
  • jvm指令中前面的位置是我們的執行地址(物理地址),中間是相關執行指令,最後面帶#是常量地址(我們後面會講到)
  • 我們的jvm程式碼是不能直接與cpu交互的,我們需要通過解釋器將jvm程式碼編程機器碼,才可以與cpu進行交互
  • 但是我們的jvm程式碼的位置不是順序排列的,所以這時我們每個執行緒都需要一個程式計數器來記錄下一個jvm的位置
  • 我們將該jvm指令傳給解釋器後,解釋器將其處理的同時程式計數器也接收到下一個地址,進行jvm位置更新

同時我們也強調一點:

  • 程式計數器只是邏輯上的概念,我們通常採用暫存器來充當一個程式計數器
  • 因為暫存器的讀取速度是最快的,我們可以快速保存並且讀出物理地址位置來進行交互

虛擬機棧

這小節我們來介紹JVM記憶體結構中的虛擬機棧

棧簡介

我們首先來回顧棧的概念:

我們的棧先進後出,用於存儲程式中的部分資訊

虛擬機棧簡介

我們的虛擬機棧和棧的基本原理相同,但存儲的東西就不盡相同了:

  • 虛擬機棧也是綁定執行緒的,每個執行緒有且僅有一個虛擬機棧
  • 虛擬機棧中存儲著棧幀,可以存在有多個棧幀,棧幀就是每個方法運行時所需要的記憶體

我們給出虛擬機的概念:

  • 每個執行緒運行時所需要的記憶體,稱為虛擬機棧
  • 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法
  • 每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所佔用的記憶體

虛擬機棧詳細介紹

我們給出一段Java程式碼來進行展示:

package cn.itcast.jvm.t1.stack;

/**
 * 演示棧幀
 */
public class Demo1_1 {
    public static void main(String[] args) throws InterruptedException {
        method1();
        
        method3();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c =  a + b;
        return c;
    }
    
    private static int method3() {
        return 1;
    }
    
}

我們進行簡單的介紹:

  • 這個程式就是一個執行緒
  • 這三個方法分別就對應著一個棧幀
  • 我們調用main,mian進入棧,main中又調用method1,method1進入棧,method1調用method2,所以method2進入棧
  • 注意我們的method3不包含在method1的循環中,所以我們會先將前置棧幀都排除後,然後在main棧幀的上方進行累加method3

我們在執行過程中如果採用debug模式是可以看到Frames,這個就是表示的棧幀:

虛擬機棧問題解釋

我們針對虛擬機棧提出了三個問題,下面進行解釋:

  1. 垃圾回收是否會涉及棧記憶體
/*
答案是:

否,因為棧是屬於執行緒內記憶體,棧具有自動回收功能
*/
  1. 棧記憶體是否是越大越好
/*
答案是:

否,如果jvm設置的記憶體過大,就會導致其它程式所佔用的記憶體小。
*/
  1. 方法內的局部變數是否執行緒安全
/**
答案是:

根據實際情況而定,我們通過判斷作用範圍來進行執行緒安全判斷

首先我們需要介紹兩個辭彙:
- StringBuffer 用於多執行緒,保證多執行緒安全,但效率較慢
- StringBuilder 用於單執行緒,無法保證但執行緒安全,效率較快

**/

// 我們下面給出一個簡單示例:

// 示例1:下面的變數x是屬於方法中的變數,屬於局部變數,因此不會出現執行緒安全問題

package cn.itcast.jvm.t1.stack;

/**
 * 局部變數的執行緒安全問題
 */
public class Demo1_18 {

    // 多個執行緒同時執行此方法
    static void m1() {
        int x = 0;
        for (int i = 0; i < 5000; i++) {
            x++;
        }
        System.out.println(x);
    }
}

// 示例2:下面我們通過StringBuilder來進行安全問題測試(因為StringBuilder不具有執行緒安全保護)

// m1:StringBuilder創建在方法內部,也只在方法內部使用,屬於局部變數,不具有執行緒安全問題
// m2:StringBuilder來自於外部,變數作用範圍越界,具有執行緒安全問題
// m3:StringBuilder返回至外部,變數作用範圍越界,具有執行緒安全問題

package cn.itcast.jvm.t1.stack;

/**
 * 局部變數的執行緒安全問題
 */
public class Demo1_17 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        }).start();
    }

    public static void m1() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static void m2(StringBuilder sb) {
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }
}

虛擬機棧記憶體溢出問題

虛擬機棧在默認情況下為1024K,正常情況下不會溢出,但如果出現異常可能導致溢出

首先我們介紹一個改變虛擬機棧的方法:

// 在配置運行環境的Environment variables中進行配置(如下修改為256k)
-Xss256k

然後我們介紹兩種溢出情況:

  1. 棧幀過多
// 正常情況下我們的棧幀(方法)就算再多也不會導致記憶體溢出,但是如果我們發生了無限遞歸異常呢?

// 我們在這個方法中遞歸調用本身,就會導致不斷有棧幀加入到虛擬機棧中,最終導致虛擬機棧記憶體溢出

package cn.itcast.jvm.t1.stack;

/**
 * 演示棧記憶體溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1() {
        count++;
        method1();
    }
}
  1. 棧幀過大
/*
我們僅僅是來解釋這個溢出想法
但實際上我們的默認虛擬機棧大小為1M,是不可能出現棧幀過大的情況的~
*/

虛擬機執行緒實際問題運行判斷

我們會給出兩個實際案例來進行講解:

  1. CPU佔用過多
// 我們的項目通常都會運行在Linux伺服器上,所以我們下面通過Linux來介紹方法

// 首先通過top定位哪個進程對cpu的佔用過高
top 
    
// 然後我們通過ps命令進一步查看哪個執行緒引起cpu佔用率過高
ps H -eo pid,tid,%cpu | grep 進程id
    
// 最後我們查看執行緒具體問題
jstack 進程id
    
// 最後我們到我們的項目程式碼中進行檢查會發現問題(可能是死循環之類的)
package cn.itcast.jvm.t1.stack;

/**
 * 演示 cpu 佔用過高
 */
public class Demo1_16 {

    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}
  1. 程式運行過久沒有結果
/*
我們採用之前相同的方法來進行判斷,一般運行過久沒有結果都是發生死鎖問題
*/

package cn.itcast.jvm.t1.stack;

/**
 * 演示執行緒死鎖
 */
class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我獲得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我獲得了 a 和 b");
                }
            }
        }).start();
    }

}

本地方法棧

這小節我們來介紹JVM記憶體結構中的本地方法棧

本地方法簡介

首先我們先來簡單介紹一下本地方法:

  • JVM屬於Java層次的東西,是無法通過Java與底層進行交互
  • 這時我們就需要一些採用C,C++語言的方法來與底層進行交互,這種方法就被稱為本地方法

本地方法特點:

  • 本地方法大多設置為介面,其返回值類型為native
  • 我們常見的本地方法包括有Object中的clone方法,hashCode方法,wait方法等

本地方法棧簡介

本地方法棧自然也不難理解:

  • 本地方法棧就是一個存儲本地方法的棧
  • 其原理與虛擬機棧完全相同,只不過裡面的棧幀變為了本地方法而已

這小節我們來介紹JVM記憶體結構中的堆

堆簡介

首先我們需要先理解什麼是堆:

  • 堆的本體通常可以被看做一棵完全二叉樹的數組

那麼堆裡面通常會儲存什麼東西:

  • 通過關鍵字new創建的對象都會使用堆來存儲

堆具有以下基本特點:

  • 有垃圾回收機制
  • 堆是執行緒共享的,堆中的所有對象都需要考慮執行緒安全問題

堆記憶體溢出問題

堆通常是用於存儲new創建的對象,它的默認大小同樣為1024K,我們提供方法來改變堆記憶體:

// 在配置運行環境的Environment variables中進行配置(如下修改為256k)
-Xmx8m

堆出現記憶體溢出問題只有一種情況就是創建對象過多:

/*
正常情況下,我們的創建的對象在不使用的情況下就會被自動垃圾回收
但如果出現異常,導致我們不斷創建新對象且保存就對象就會導致堆記憶體溢出
*/

package cn.itcast.jvm.t1.heap;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆記憶體溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // 這裡將舊對象保存下來
                a = a + a;  // 這裡不斷創建新對象
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

堆記憶體問題診斷

我們在正常運行中堆的記憶體佔有是非常重要,因此JVM為我們提供了四種方法來檢查堆記憶體問題

首先我們給出用於診斷堆記憶體問題的參考程式碼:

package cn.itcast.jvm.t1.heap;

/**
 * 演示堆記憶體
 */
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        // 第一階段:沒有對象
        System.out.println("1...");
        Thread.sleep(30000);
        // 第二階段:製造一個對象,佔用堆
        byte[] array = new byte[1024 * 1024 * 10]; 
        System.out.println("2...");
        Thread.sleep(20000);
        // 第三階段:釋放對象,並進行垃圾回收,這時堆變小
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

我們的JVM為我們提供了四種方法來檢測堆的狀況:

  1. jps工具
// jps用於查看當前系統中有哪些java進程
// 我們直接在IDEA的輸入台輸入即可
jps
  1. jmap工具
// jmap用於查看當前系統中堆記憶體佔用情況(靜態形式)
// 我們直接在IDEA的輸入台輸入即可
jmap -heap 進程id
    
// 我們可以看到Heap Usage就是記憶體管理
// 其中Eden space 為新產生的堆記憶體
// 其中Old Generation 為之前產生的堆記憶體
  1. jconsole工具
// jconsole用於查看當前系統中堆記憶體佔用情況(圖形化介面app展示)
// 我們直接在IDEA的輸入台輸入即可
jconsole
  1. jvisualvm工具
// jvisualvm用於查看當前系統中堆記憶體佔用情況(圖形化介面app展示)
// 我們直接在IDEA的輸入台輸入即可
jvisualvm

方法區

這小節我們來介紹JVM記憶體結構中的方法區

方法區簡介

我們首先來簡單介紹一下方法區:

  • 方法區是所有java虛擬機共享的一片區域
  • 方法區中存放著所有類的所有資訊,包括有屬性,方法,構造方法等
  • 方法區在虛擬機啟動的一瞬間被創建,同樣在虛擬機停止時方法區進行銷毀

我們需要特別注意一點:

  • 方法區和程式計數器一樣只是一個概念
  • 我們在實際開發中,jdk1.8之前採用的是永久代,在jdk1.8及以後均採用元空間

我們直接給出其記憶體結構圖展示:

方法區記憶體溢出問題

方法區同樣存在有記憶體溢出問題,但並不常見

我們將方法區的講解分為兩部分,有永久代也有元空間的講解:

  1. 永久代記憶體溢出問題
// 永久代的概念僅存在於jdk1.8之前,我們可以通過-XX來控制永久代大小

// 當方法區為永久代時,溢出就顯示錯誤java.lang.OutOfMemoryError: PermGen space

package cn.itcast.jvm;


import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;

/**
 * 演示永久代記憶體溢出  java.lang.OutOfMemoryError: PermGen space
 * -XX:MaxPermSize=8m
 */
public class Demo1_8 extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 20000; i++, j++) {
                ClassWriter cw = new ClassWriter(0);
                cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = cw.toByteArray();
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}
  1. 元空間記憶體溢出問題
// 元空間存在於jdk1.8之後,實際上這時的元空間已經作用於系統記憶體了,相當於元空間的大小几乎是不可能出現溢出的

// 所以我們需要先設置元空間大小才能觀察到溢出問題:-XX:MaxMetaspaceSize=8m

// 當方法區為永久代時,溢出就顯示錯誤java.lang.OutOfMemoryError: Metaspace

package cn.itcast.jvm.t1.metaspace;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空間記憶體溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用來載入類的二進位位元組碼
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成類的二進位位元組碼
                ClassWriter cw = new ClassWriter(0);
                // 版本號, public, 類名, 包名, 父類, 介面
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 執行了類的載入
                test.defineClass("Class" + i, code, 0, code.length); // Class 對象
            }
        } finally {
            System.out.println(j);
        }
    }
}

常量池簡介

我們再回到方法區來簡單介紹一下常量池:

  • 我們在上面的圖中可以看到常量池之前是放在方法區中的StringTable,但在jdk1.8之後放在了堆中的StingTable
  • 我們需要注意的是:即使StringTable在堆裡面,在堆里存放的數據和在StringTable里存放的數據也不是同一個數據

我們先來簡單介紹一下常量池:

  • 常量池實際上是一張表
  • 虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等資訊

我們再介紹一下運行時常量池:

  • 運行時常量池,常量池是 *.class 文件中的

  • 當該類被載入,它的常量池資訊就會放入運行時常量池,並把裡面的符號地址變為真實地址

常量池詳細介紹

我們通過一個簡單的程式碼編譯來介紹常量池:

// 下面是helloworld的源碼

package cn.itcast.jvm.t5;

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

然後我們運行之後,我們可以在out文件夾下找到其編譯後程式:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.itcast.jvm.t5;

public class HelloWorld {
    public HelloWorld() {
    }

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

我們在該目錄下對其進行底層查看:

// 我們通過javap -v 程式碼名.class來查看其詳細資訊
// 其中包括有:class文件的路徑、最後修改時間、文件大小;類的全路徑、源(java)文件;常量池;常量定義、值;構造方法等
javap -v HelloWorld.class
    
// 我們查看其內部詳細資訊:
    
// 這部分是class文件路徑,最後修改日誌,文件大小等資訊
Classfile /E:/編程內容/JVM/資料-解密JVM/程式碼/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2022-11-2; size 567 bytes
  MD5 checksum 8efebdac91aa496515fa1c161184e354
  Compiled from "HelloWorld.java"

// 這部分是全路徑,源碼等
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
      
// 這部分是常量池:我們可以看到很多東西,註解是IDEA為我們攜帶的
// 首先我們可以看到最前面的#,這個代表這一行的地址,然後如果我們希望看懂這一行的資訊,需要根據後面的#查看對應的行號
// 我們到對應的行號去尋找,直到最後我們可以看到utf8形式的結果,我們將這些資訊組合起來就是該行後面IDEA為我們注釋的資訊
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
                            
// 這部分是編譯後的程式碼,我們可以看到裡面包含了#號,這些#就對應著上面的常量池,他們從常量池中獲得相關資訊用於程式碼中
{
  public cn.itcast.jvm.t5.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
                  
      // 這裡是局部變數表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
                                     
// 最後標上上述資訊的來源文件
SourceFile: "HelloWorld.java"

StringTable串池簡介

我們首先來簡單介紹一下串池:

  • 串池的本質是一個哈希表,其中的每個元素都是唯一的

我們在這裡稍微解釋一下為什麼StringTable會移動到堆中:

  • jdk7中將StringTable放到了堆空間中
  • 因為永久代的回收效率很低,在full GC的時候才會觸發。
  • 而Full GC是老年代空間不足、永久代空間不足時才會觸發。這就導致StringTable回收效率不高。
  • 而我們開發中會有大量的字元串被創建,回收效率低,導致永久代記憶體不足。放到堆里,能及時回收記憶體。

然後我們提前介紹一下串池的特點:

  • 常量池的字元串僅僅是符號,第一次用到時才會變為對象
  • 利用串池的機制,可以避免重複創建字元串對象
  • 字元串變數拼接的原理是StringBuiler拼接(jdk1.8)
  • 字元串常量拼接的原理是編譯期優化
  • 可以使用intern方法,主動將串池中還沒有的字元串放入串池

StringTable串池詳細介紹

我們通過一段程式碼來仔細介紹串池:

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ]  hashtable 結構,不能擴容
public class Demo1_22 {
    // 常量池中的資訊,都會被載入到運行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字元串對象

    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 == s5);
    }
}

然後我們對其進行編譯解碼:

// 解碼語句
javap -v Demo1_22.class
    
// 基本資訊
Classfile /E:/編程內容/JVM/資料-解密JVM/程式碼/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2022-11-2; size 985 bytes
  MD5 checksum a5eb84bf1a7d8a1e725491f36237777b
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
      
// 常量池
Constant pool:
   #1 = Methodref          #12.#36        // java/lang/Object."<init>":()V
   #2 = String             #37            // a
   #3 = String             #38            // b
   #4 = String             #39            // ab
   #5 = Class              #40            // java/lang/StringBuilder
   #6 = Methodref          #5.#36         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#41         // java/lang/StringBuilder.append:
   #8 = Methodref          #5.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #45.#46        // java/io/PrintStream.println:(Z)V
  #11 = Class              #47            // cn/itcast/jvm/t1/stringtable/Demo1_22
  #12 = Class              #48            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               s1
  #25 = Utf8               Ljava/lang/String;
  #26 = Utf8               s2
  #27 = Utf8               s3
  #28 = Utf8               s4
  #29 = Utf8               s5
  #30 = Utf8               StackMapTable
  #31 = Class              #23            // "[Ljava/lang/String;"
  #32 = Class              #49            // java/lang/String
  #33 = Class              #50            // java/io/PrintStream
  #34 = Utf8               SourceFile
  #35 = Utf8               Demo1_22.java
  #36 = NameAndType        #13:#14        // "<init>":()V
  #37 = Utf8               a
  #38 = Utf8               b
  #39 = Utf8               ab
  #40 = Utf8               java/lang/StringBuilder
  #41 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #53:#54        // toString:()Ljava/lang/String;
  #43 = Class              #55            // java/lang/System
  #44 = NameAndType        #56:#57        // out:Ljava/io/PrintStream;
  #45 = Class              #50            // java/io/PrintStream
  #46 = NameAndType        #58:#59        // println:(Z)V
  #47 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/String
  #50 = Utf8               java/io/PrintStream
  #51 = Utf8               append
  #52 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #53 = Utf8               toString
  #54 = Utf8               ()Ljava/lang/String;
  #55 = Utf8               java/lang/System
  #56 = Utf8               out
  #57 = Utf8               Ljava/io/PrintStream;
  #58 = Utf8               println
  #59 = Utf8               (Z)V
                            
// 程式碼解釋
{
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t1/stringtable/Demo1_22;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
                  
         // 從這裡開始我們正式進入main
         
         // 在最開始:StringTable=[],堆=[]         
         
         // ldc #2 會把 a 符號變為 "a" 字元串對象,這時StringTable=["a"] 
         0: ldc           #2                  // String a
         // 這裡的astore_1意思將#2的值放入局部變數池的第一位
         2: astore_1
         // ldc #3 會把 b 符號變為 "b" 字元串對象,這時StringTable=["a","b"] 
         3: ldc           #3                  // String b
         5: astore_2
         // ldc #4 會把 ab 符號變為 "ab" 字元串對象,這時StringTable=["a","b","ab"] 
         6: ldc           #4                  // String ab
         8: astore_3
                  
         // 接下來的操作都是針對String s4 = s1 + s2;         
                  
         // 這裡首先創建了一個StringBuilder類               
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
         // 這裡針對StringBuilder進行初始化
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
         // 這裡對StringBuilder進行append方法,上面的aload_1意思是讀取了第一個局部變數的值,相當於添加了"a"
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:
        20: aload_2
         // 這裡對StringBuilder進行append方法,上面的aload_1意思是讀取了第二個局部變數的值,相當於添加了"b"
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:
         // 這裡對StringBuilder進行toString方法,相當於new了一個"ab",這時StringTable沒有發生變化,但是堆產生了該值
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:
                  
         // 這裡是針對String s5 = "a" + "b";操作,由於產生的結果為"ab",已經放在局部變數表裡,所以直接讀取即可
         // 注意:有StringTable的值只能有一個,所以這時StringTable=["a","b","ab"] 並沒有發生變化
        27: astore        4
        29: ldc           #4                  // String ab
                  
        // 後面就是獲得對應值然後比較
        31: astore        5
        33: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: aload_3
        37: aload         5
        39: if_acmpne     46
        42: iconst_1
        43: goto          47
        46: iconst_0
        47: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        50: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 14: 9
        line 15: 29
        line 17: 33
        line 21: 50
                  
      // 局部變數池 
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      51     0  args   [Ljava/lang/String;
            3      48     1    s1   Ljava/lang/String;
            6      45     2    s2   Ljava/lang/String;
            9      42     3    s3   Ljava/lang/String;
           29      22     4    s4   Ljava/lang/String;
           33      18     5    s5   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 46
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo1_22.java"

StringTable字元串延遲載入

在這裡我們再次強調一下StringTable中元素的載入原則:

  • StringTable中的值只會載入一次,不會重複載入
  • 存放在常量池的值在運行時不會載入,只有在第一次運行時才會載入到StringTable中

我們採用一個簡單程式來證明:

package cn.itcast.jvm.t1.stringtable;

/**
 * 演示字元串字面量也是【延遲】成為對象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字元串個數 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字元串個數 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字元串個數 2285
    }
}

StringTable的intern功能介紹

我們的intern的功能主要分為兩個版本:

  • jdk1.6:將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象複製一份,放入串池, 會把串池中的對象返回
  • jdk1.8:將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的對象返回

我們利用同樣的程式碼來進行不同版本的介紹:

// 1.6版本

package cn.itcast.jvm;

public class Demo1_23 {

    public static void main(String[] args) {

		// 我們來仔細分析這個操作
        // 首先StringTable裡面加入"a",然後堆里加上一個"a";然後StringTable裡面加入"b",然後堆里加上一個"b"
        // 最後使用了toString方法,將"ab"放入堆中
        String s = new String("a") + new String("b");

        // StringTable=["a","b"]
        // 堆:  new String("a")   new String("b")  new String("ab")

        // 將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的對象返回
        
        // 目前s沒有在StringTable中,所以將其複製一份放入StringTable,並將StringTable裡面的"ab"返回回去
        // 這時s和StringTable裡面的"ab"是不一樣的!
        String s2 = s.intern(); 

        // 這時x,s2是StringTable裡面的"ab",s是堆裡面的"ab"
        String x = "ab";
        
        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }

}

    public static void main(String[] args) {

        String x = "ab";
        
        String s = new String("a") + new String("b");

        // StringTable=["a","b","ab"]
        // 堆  new String("a")   new String("b") new String("ab")
        // 將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池,會把串池中的對象返回
        // 目前StringTable里存在"ab",所以將StringTable裡面的ab返回給s2即可
        // 目前s2和x屬於StringTable裡面的ab,s屬於堆裡面的ab
        String s2 = s.intern(); 

        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }

}
// 1.8版本

package cn.itcast.jvm.t1.stringtable;

public class Demo1_23 {

    public static void main(String[] args) {

        
        String s = new String("a") + new String("b");

        // StringTable=["a","b"]
        // 堆  new String("a")   new String("b") new String("ab")
        // 將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池,會把串池中的對象返回
        // 這裡由於StringTable裡面不存在,會將堆中s的字元串做成一個引用直接放入StringTable裡面,再將StringTable的值返回
        // 這時s,s2,x均屬於堆裡面的ab,不過s2是堆里的ab,s,x為StringTable裡面的引用的堆裡面的ab,但他們相等
        String s2 = s.intern(); 

        String x = "ab";
        
        System.out.println( s2 == x);//true
        System.out.println( s == x );//true
    }

}

    public static void main(String[] args) {

        String x = "ab";
        
        String s = new String("a") + new String("b");

        // StringTable=["a","b","ab"]
        // 堆  new String("a")   new String("b") new String("ab")
        // 將這個字元串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池,會把串池中的對象返回
        // 目前StringTable里存在"ab",所以將StringTable裡面的ab返回給s2即可
        // 目前s2和x屬於StringTable裡面的ab,s屬於堆裡面的ab
        String s2 = s.intern(); 

        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }

}

StringTable常見面試題解答

下面我們給出一個StringTable的常見面試題來進行測試:

package cn.itcast.jvm.t1.stringtable;

/**
 * 演示字元串相關面試題
 */
public class Demo1_21 {

    // 我們會給出StringTable和堆的值
    
    public static void main(String[] args) {
        
        // StringTable=["a"]
        String s1 = "a";
        
        // StringTable=["a","b"]
        String s2 = "b";
        
        // StringTable=["a","b","ab"]
        String s3 = "a" + "b"; // ab
        
        // StringTable=["a","b","ab"],堆:"ab"
        String s4 = s1 + s2;   // new String("ab") 和 s3 不相等
        
        // StringTable=["a","b","ab"],堆:"ab"
        String s5 = "ab"; // s3 == s5
        
        // StringTable=["a","b","ab"],堆:"ab"
        String s6 = s4.intern(); // s4是堆里的ab,s6是StringTable裡面的ab

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

        // StringTable=["a","b","ab","c","d"],堆:"ab"."c","d","cd"
        String x2 = new String("c") + new String("d"); // new String("cd")
        
        // StringTable=["a","b","ab","c","d","cd-來自堆"],堆:"ab"."c","d","cd"
        x2.intern();
        
        // x1是StringTable裡面的cd,但StringTable裡面的cd來自堆,所以x1 == x2
        String x1 = "cd";
        
        System.out.println(x1 == x2);

// 問,如果調換了【最後兩行程式碼】的位置呢,如果是jdk1.6呢(這個就自己思考啦~)
       
    }
}

StringTable垃圾回收問題

StringTable會自動進行垃圾回收,這也是我們在JDK運行中選擇StringTable的原因之一

我們通過一個簡單的案例進行解釋:

package cn.itcast.jvm.t1.stringtable;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics(列印StringTable內容) -XX:+PrintGCDetails -verbose:gc(列印垃圾回收)
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // 設置1w個數
                // 這裡如果不採用.intern()
                // 我們會發現1w個數全部存入記憶體中
                String.valueOf(j);
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics(列印StringTable內容) -XX:+PrintGCDetails -verbose:gc(列印垃圾回收)
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // 設置1w個數
                // 這裡如果採用.intern()
                // 我們會發現StringTable裡面的值會少於1w,這是因為發生了垃圾回收,回收掉不使用的資訊
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

StringTable調優

最後我們介紹StringTable的調優方法:

  1. 設置桶的個數
// 我們直到StringTable是一個哈希表,哈希表裡面桶的個數會影響其效率
// 如果桶過少,每個桶存儲資訊過多導致查找緩慢;如果桶過多,導致資訊分布較為疏散導致查找緩慢
// 我們提供一個配置來改變桶的個數:-XX:StringTableSize=10000(需要設計恰到好處的桶個數)

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小對性能的影響
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }


    }
}
  1. 考慮字元串對象是否入池
// 我們同樣可以採用intern來判斷該字元串是否應該入池
// 我們排除掉相同的字元串自然可以節省空間~

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示 intern 減少記憶體佔用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    // 如果這裡不採用.tern會導致全部字元串進入,導致儲存較多
                    address.add(line);
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

/**
 * 演示 intern 減少記憶體佔用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    // 如果這裡採用.tern就會篩選字元串,來進行調優~
                    address.add(line.tern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

直接記憶體

這小節我們來介紹系統中常用的直接記憶體

直接記憶體簡介

我們先來介紹一下直接記憶體的定義:

  • 直接記憶體不受JVM記憶體回收管理
  • 直接記憶體是直接受管於系統的記憶體,不能被JVM所調配
  • 直接記憶體通常用於NIO操作,用於數據緩衝區,其分配成本較高,但讀寫性能較高

直接記憶體詳細介紹

我們通過一段程式碼來展示直接記憶體的速度:

package cn.itcast.jvm.t1.direct;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\編程資料\\第三方教學影片\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用時:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用時:479.295165 702.291454 562.56592
    }

    // directBuffer(直接記憶體讀取數據)
    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用時:" + (end - start) / 1000_000.0);
    }

    // io(jvm正常讀取數據)
    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用時:" + (end - start) / 1000_000.0);
    }
}

我們可以明顯看到directBuffer速度比IO讀取快很多,那麼究竟是怎麼實現的

我們可以分別給出兩張圖進行解釋:

  1. JVM正常讀取

  1. 直接記憶體讀取

我們由上圖可以得知:

  • JVM正常讀取需要先複製一份經過系統記憶體緩衝區,然後再複製一份才能進入到java文件中
  • DirectMemory可以同時在系統記憶體和java堆記憶體中使用,我們只需要傳入數據到直接記憶體中就可以直接讀取調用

直接記憶體記憶體溢出問題

我們同樣來進行直接記憶體的記憶體溢出問題測試:

package cn.itcast.jvm.t1.direct;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


/**
 * 演示直接記憶體溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                // 這裡設置一個大小為100mb的直接記憶體
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法區是jvm規範, jdk6 中對方法區的實現稱為永久代
        //                  jdk8 對方法區的實現稱為元空間
    }
}

直接記憶體釋放原理

我們目前所使用的直接記憶體是DirectMemory:

package cn.itcast.jvm.t1.direct;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * 我們查看記憶體管理需要到任務管理器里查看,因為該記憶體屬於系統記憶體,不再屬於jvm
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    // 我們使用debug模式調試
    
    public static void main(String[] args) throws IOException {
        // 我們使用byteBuffer來調取1G的記憶體使用
        
        // 我們開啟項目後會看到一個記憶體為1G的java項目
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完畢...");
        
        // 輸入空格後開始進行系統的垃圾回收,這時byteBuffer被回收,我們會注意到記憶體為1G的項目結束
        System.in.read();
        System.out.println("開始釋放...");
        byteBuffer = null;
        System.gc();
        System.in.read();
    }
}

但是我們需要注意的是我們的jvm的回收功能對系統記憶體是沒有管轄權力的

所以回收ByteBuffer的類另有他人:

package cn.itcast.jvm.t1.direct;

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * 直接記憶體分配的底層原理:Unsafe
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        
        // unsafe正常情況下不會使用,因為系統記憶體通常由系統自動控制,我們這裡採用暴力反射獲取
        Unsafe unsafe = getUnsafe();
        
        // 分配記憶體(base實際上是該記憶體的地址,所以我們在釋放時同樣提供該base地址)
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 釋放記憶體
        unsafe.freeMemory(base);
        System.in.read();
    }

    // 暴力反射獲得unsafe對象
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

然後我們就可以通過DirectMemory的源碼來查看為什麼它會收到jvm控制:

    // Primary constructor
    // DirectByteBuffer構造器裡面直接調用了unsafe類來進行直接記憶體的控制
    DirectByteBuffer(int cap) {

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        // 進行直接記憶體的生產
        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        
        // cleaner會自動檢測directMemory是否還存在,若不存在調用該方法
        // 這裡採用cleaner,直接創建一個新的類型的Deallocator,跳轉到下面的類中
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }


	// cleaner操作跳轉的類,繼承了Runnable
    private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
		
        // 被清理時調用下述方法,採用unsafe.freeMemory(address)來清理直接記憶體,所以我們的垃圾回收才能清理直接記憶體
        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

禁用顯式回收的影響

其實在正常情況下我們的顯式回收是不被允許開啟的,因為可能會導致我們的部分資訊損失:

package cn.itcast.jvm.t1.direct;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * 禁用顯式回收對直接記憶體的影響
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 禁止顯式回收配置
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完畢...");
        System.in.read();
        System.out.println("開始釋放...");
        byteBuffer = null;
        System.gc(); // 顯式的垃圾回收,Full GC 這時這個操作是無效的
        System.in.read();
        
        // 那麼直接記憶體只能等到系統記憶體滿了之後自動調用被動垃圾回收,但那樣直接記憶體會佔用大量空間
        // 但是我們又希望清除掉這個直接記憶體,那麼我們這時就只能手動採用unsafe的方法了,這裡就不做程式碼展示了~
        // unsafe.freeMemory(address);
    }
}

結束語

到這裡我們JVM的記憶體結構篇就結束了,希望能為你帶來幫助~

附錄

該文章屬於學習內容,具體參考B站黑馬程式設計師滿老師的JVM完整教程

這裡附上影片鏈接:01_什麼是jvm_嗶哩嗶哩_bilibili