JVM 內存結構


一、前言

1.1、什么是 JVM ?

1)定義
Java Virtual Machine ,Java 程序的運行環境(Java 二進制字節碼的運行環境)。
2)好處

  •     一次編譯,處處執行
  •     自動的內存管理,垃圾回收機制
  •     數組下標越界檢查

3)比較
JVM、JRE、JDK 的關系如下圖所示

1.2、學習 JVM 有什么用?

    面試必備
    中高級程序員必備
    想走的長遠,就需要懂原理,比如:自動裝箱、自動拆箱是怎么實現的,反射是怎么實現的,垃圾回收機制是怎么回事等待,JVM 是必須掌握的。

1.3、常見的 JVM

一套規范,可以自己實現jmv的

我們主要學習的是 HotSpot 版本的虛擬機。

HotSpot VM是Sun JDK和OpenJDK中所帶的虛擬機。

1.4、學習路線

ClassLoader:Java 代碼編譯成二進制后,會經過類加載器,這樣才能加載到 JVM 中運行。
Method Area:類是放在方法區中。
Heap:類的實例對象。
當類調用方法時,會用到 JVM Stack、PC Register、本地方法棧。

方法執行時的每行代碼是有執行引擎中的解釋器逐行執行,方法中的熱點代碼頻繁調用的方法,由 JIT 編譯器優化后執行,GC 會對堆中不用的對象進行回收。需要和操作系統打交道就需要使用到本地方法接口(調用操作系統方法)。

二、內存結構

2.1、程序計數器

1)定義
Program Counter Register 程序計數器(寄存器)
作用:是記錄下一條 jvm 指令的執行地址行號。
特點:

  •     是線程私有的
  •     不會存在內存溢出

2)作用

計數器是java對物理硬件(寄存器)的屏蔽和抽象

解釋器會解釋指令為機器碼交給 cpu 執行,程序計數器會記錄下一條指令的地址行號,這樣下一次解釋器會從程序計數器拿到指令然后進行解釋執行。

多線程的環境下,如果兩個線程發生了上下文切換,那么程序計數器會記錄線程下一行指令的地址行號,以便於接着往下執行。

2.2、虛擬機棧

1)定義

每個線程運行需要的內存空間,稱為虛擬機棧

每個棧由多個棧幀(Frame)組成,對應着每次調用方法時所占用的內存

每個線程只能有一個活動棧幀,對應着當前正在執行的方法

 

棧頂的那個棧幀,調用一次方法,把方法的棧幀放入棧,方法執行完,彈出棧幀;方法調用方法,在放入另一個棧幀。

問題辨析:

垃圾回收是否涉及棧內存?

不會。棧內存是方法調用產生的,方法調用結束后會彈出棧。

棧內存分配越大越好嗎?
不是。因為物理內存是一定的,棧內存越大,可以支持更多的遞歸調用,但是可執行的線程數就會越少。因為一個線程對應一個棧,即棧是線程私有的,所以棧大,那么棧數目少,線程數就少。

方法的局部變量是否線程安全?

如果方法內部的變量沒有逃離方法的作用訪問,它是線程安全的
如果是局部變量引用了對象,並逃離了方法的訪問,那就要考慮線程安全問題(函數參數、 函數返回值等的情況)。

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;
    }
}

m1線程安全,私有的引用局部變量

m2線程不安全,sb是方法參數傳遞的,說明與其他線程共享

m3線程不安全,作為返回值,也共享了


2)棧內存溢出

棧幀過大(局部變量一般占用內存比較少,不太容易出現)、

過多(方法調用太多,且沒有返回,遞歸沒有終止)、

或者第三方類庫操作(兩個類的循環引用,json循環依賴),

都有可能造成棧內存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定棧內存大小!

 

3)線程運行診斷
案例一:cpu 占用過多
解決方法:Linux 環境下運行某些程序的時候,可能導致 CPU 的占用過高,這時需要定位占用 CPU 過高的線程

top 命令,查看是哪個進程占用 CPU 過高
ps H -eo pid, tid(線程id), %cpu | grep 剛才通過 top 查到的進程號 通過 ps 命令進一步查看是哪個線程占用 CPU 過高

jstack 進程 id 通過查看進程中的線程的 nid ,剛才通過 ps 命令看到的 tid 來對比定位,注意 jstack 查找出的線程 id 是 16 進制的,需要轉換。

2.3、本地方法棧

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

Object中有很多本地方法,clone/wait/..

2.4、堆

1)定義

Heap 堆

    通過new關鍵字創建的對象都會被放在堆內存

特點

  •     它是線程共享,堆內存中的對象都需要考慮線程安全問題
  •     有垃圾回收機制,堆中不再引用的對象會被釋放內存


2)堆內存溢出

java.lang.OutofMemoryError :java heap space. 堆內存溢出,堆中的對象太多,也沒被回收
可以使用 -Xmx8m 來指定堆內存大小(大小指定8M)。

先是將hello對象創建堆,將對象引用加入list集合

然后不斷做字符串拼接,將hello*****對象創建堆,將對象引用加入list集合

。。。。。死循環,對象也無法回收,爆了。

3)堆內存診斷

  •     jps 工具

查看當前系統中有哪些 java 進程

  •     jmap 工具

    查看堆內存占用情況 jmap - heap 進程id

  •     jconsole 工具

    圖形界面的,多功能的監測工具,可以連續監測

  •     jvisualvm 工具

先運行演示堆內存的程序

/**
 * 演示堆內存
 */
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]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

 idea Terminal中運行jps

查看當前系統中有哪些 java 進程

I:\網課資料\資料-解密JVM\代碼\jvm>jps
22080 Jps
21556
23380 Demo1_4
5812 RemoteMavenServer36
8460 Launcher

 

查看堆內存占用情況

內存快照信息

I:\網課資料\資料-解密JVM\代碼\jvm>jmap -heap 23380
Attaching to process ID 23380, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4261412864 (4064.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1420296192 (1354.5MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 17145656 (16.35137176513672MB)
   free     = 49438920 (47.14862823486328MB)
   25.750191756120817% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

3170 interned Strings occupying 280952 bytes.

jconsole 工具

idea Terminal中運行jconsole

可以看到堆內存空間先增后降 符合代碼

2.5、方法區

2.5.1 定義

Java 虛擬機有一個在所有 Java 虛擬機線程之間共享的方法區域

方法區域類似於用於傳統語言的編譯代碼的存儲區域,或者類似於操作系統進程中的“文本”段。

它存儲每個類的結構,例如運行時常量池、字段和方法數據,以及方法和構造函數的代碼,包括特殊方法,用於類和接口的實例初始化。

方法區域是在虛擬機啟動時創建的。

盡管方法區在邏輯上是堆的一部分(不同廠商實現不一樣,HotSpots 1.8前是永久代,堆的一部分,1.8時把永久代移除了,元空間,本地系統內存),但簡單的實現可能不會選擇垃圾收集或壓縮它。

方法區是規范,什么永久代、元空間是實現。

此規范不強制指定方法區的位置或用於管理已編譯代碼的策略。方法區域可以具有固定的大小,或者可以根據計算的需要進行擴展,並且如果不需要更大的方法區域,則可以收縮。

方法區域的內存不需要是連續的!


2.5.2 組成

Hotspot 虛擬機 jdk1.6 1.7 1.8 內存結構圖

ClassLoader用來加載類的字節碼。

2.5.3 方法區內存溢出

    1.8 之前會導致永久代內存溢出
        使用 -XX:MaxPermSize=8m 指定永久代內存大小
    1.8 之后會導致元空間內存溢出
        使用 -XX:MaxMetaspaceSize=8m 指定元空間大小

 

演示內存溢出

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

2.5.4 運行時常量池

運行一段程序,將程序編譯為二進制字節碼:

二進制字節碼包含(類的基本信息,常量池,類方法定義,包含了虛擬機的指令
首先看看常量池是什么,編譯如下代碼:

public class HelloWorld {
    public HelloWorld() {
    }

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

然后使用 javap -v Test.class 命令反編譯查看結果。

Classfile /I:/網課資料/資料-解密JVM/代碼/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2021-9-24; 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
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"

其中Constant pool那部分是常量池表

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

每條指令都會對應常量池表中一個地址,常量池表中的地址可能對應着一個類名、方法名、參數類型等信息。

Code后是jvm指令,指令地址   操作方式  常量池對應地址

常量池:
就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量信息
運行時常量池
常量池是 *.class 文件中的,當該類被加載以后,它的常量池信息就會放入運行時常量池(內存中),並把里面的符號地址變為真實地址(內存地址)

2.5.5 StringTable

String table又稱為String pool,字符串常量池,其存在於堆中(jdk1.7之后改的)。最重要的一點,String table中存儲的並不是String類型的對象,存儲的而是指向String對象的索引,真實對象還是存儲在堆中。

此外String table還存在一個hash表的特性,里面不存在相同的兩個字符串。

此外String對象調用intern()方法時,會先在String table中查找是否存在於該對象相同的字符串,若存在直接返回String table中字符串的引用,若不存在則在String table中創建一個與該對象相同的字符串。

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

反編譯

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
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        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

常量池中的字符串僅是符號,只有在被用到時才會將符號轉化為對象(懶漢),放入StringTable,放入時會先在StringTable中查找,如果對象存在就無法放入,不存在放入,最后返回串池中對象。
    利用串池的機制,來避免重復創建字符串對象
    字符串變量拼接的原理是StringBuilder(線程安全,效率低)

String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")

s5==s3 true
    字符串常量拼接的原理是編譯器優化,s5是常量,去常量池中查找,還特么找到了, 常量是確定,可以在編譯期間確定為ab,而引用相加不確定,只能運行時確定
    可以使用方法,主動將串池中還沒有的字符串對象放入串池中
(懶漢行為,延遲實例化,遇到一個常量,用時將常量池符號變對象,再放入StringTable)

 

intern方法 1.8
調用字符串對象的 intern 方法,會將該字符串對象嘗試放入到串池StringPooling中

  •     如果串池中沒有該字符串對象,則放入成功
  •     如果有該字符串對象,則放入失敗
  •     無論放入是否成功,都會返回串池中的字符串對象


注意:此時如果調用 intern 方法成功,堆內存與串池中的字符串對象是同一個對象;如果失敗,則不是同一個對象

例1:

public class Main {
    public static void main(String[] args) {
        // "a" "b" 被放入串池中,str 則存在於堆內存之中
        String str = new String("a") + new String("b");
        // 調用 str 的 intern 方法,這時串池中沒有 "ab" ,則會將該字符串對象放入到串池中,此時堆內存與串池中的 "ab" 是同一個對象
        String st2 = str.intern();
        // 給 str3 賦值,因為此時串池中已有 "ab" ,則直接將串池中的內容返回
        String str3 = "ab";
        // 因為堆內存與串池中的 "ab" 是同一個對象,所以以下兩條語句打印的都為 true
        System.out.println(str == st2);//true
        System.out.println(str == str3);//true
    }
}

例2:

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {

        String x = "ab";
        //此處創建字符串對象 "ab" ,因為串池中還沒有 "ab" ,所以將其放入串池中
        String s = new String("a") + new String("b");
        // "a" "b" 被放入串池中,s則存在於堆內存之中
        String s2 = s.intern(); // 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的對象返回
        // 此時因為在創建x時,"ab" 已存在與串池中,所以放入失敗,但是會返回串池中的 "ab"
        System.out.println( s2 == x); // true
        System.out.println( s == x ); // false
    }

}

當java1.6時  當調用intern方法時,如果字符串常量池先前已創建出該字符串對象,則返回池中的該字符串的引用。否則,將此字符串對象拷貝添加到字符串常量池中,並且返回該字符串對象的引用。

1.8不拷貝,1.6要拷貝,當常量池無對象時,1.8返回的引用和堆引用一樣,因為放入的是引用不是拷貝,而1.6則是常量池引用,放入的是拷貝

 

面試題

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

    public static void main(String[] args) {
        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);
    }
}

1.8 x2==x1  false

1.6 x2==x1  false

串池中已經存在“cd”了,x2不會再放入串池 x2的“cd”存在於堆中

1.8  x2==x1  true   x2一開始堆,然后將其應用放入StringPooling,x1放入后得到返回引用和x2引用一樣

1.6  x2==x1  false  x2一開始堆,然后拷貝對象放入StringPooling,x1放入后得到返回引用和x2(此時的x2還是之前的 沒有更新)不一樣

 

2.5.6 StringTable 的位置

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

因為永久代的回收效率很低,永久代只有fullGC的時候才會垃圾回收
堆中只需要minGC就可以垃圾回收,大大減少String常量對內存的占用

/**
 * 演示 StringTable 位置
 * 在jdk8下設置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下設置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

實驗 對比1.6和1.8StringPool位置

設置永久代參數,內存大小

花了98%的時間進行垃圾回收,但是垃圾回收不足2%,說明救不活了!哈哈哈 ,直接報堆溢出

2.5.7 StringTable 垃圾回收

-Xmx10m 指定堆內存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次數,耗費時間等信息

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -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++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

堆空間

 

內存不足,觸發一次垃圾回收,垃圾回收速度很快,

新生代的垃圾回收快

2.5.8 StringTable 性能調優

    * 因為StringTable是由HashTable實現的,所以可以適當增加HashTable桶(對象數組長度)的個數,減少hash碰撞的可能性,鏈的長度較短,來減少字符串放入串池所需要的時間,哈希桶的長度太小的話,如果String常量對象很多,哈希碰撞更嚴重,鏈表插入、擴容、紅黑樹費時

   *  考慮是否需要將字符串對象入池
   *  可以通過 intern 方法減少重復入池,不同對象(相同)指向池中同一String

設置桶的長度:

 -XX:StringTableSize=桶個數(最少設置為 1009 以上)

2.6、直接內存

2.6.1 定義

Direct Memory -----是操作系統的內存  ---java和系統都可以訪問,避免了內存重復

  •     常見於 NIO 操作時,用於數據緩沖區
  •     分配回收成本較高,但讀寫性能高
  •     不受 JVM 內存回收管理

2.6.2 使用直接內存的好處

文件讀寫流程:

java本身不具備磁盤讀寫的能力,需要調用操作系統的方法,本地方法--CPU狀態由用戶態(java)切換到內核態(System);

緩存,分次讀取

 

因為 java 不能直接操作文件管理,需要切換到內核態,使用本地方法進行操作,然后讀取磁盤文件,會在系統內存中創建一個緩沖區,將數據讀到系統緩沖區, 然后在將系統緩沖區數據,復制到 java 堆內存中。缺點是數據存儲了兩份,在系統內存中有一份,java 堆中有一份,造成了不必要的復制。

使用了 DirectBuffer 文件讀取流程

直接內存是操作系統和 Java 代碼都可以訪問的一塊區域,無需將代碼從系統內存復制到 Java 堆內存,從而提高了效率。磁盤文件讀取到直接內存后,可以讓java直接訪問,少了緩沖區的copy操作,所以高效,內存不浪費。

2.6.3 直接內存回收原理


1.直接內存的回收不是通過 JVM 的垃圾回收來釋放的,而是通過unsafe.freeMemory 來手動釋放。

2.ByteBuffer的實現類內部,使用了Cleaner(虛引用)來檢測ByteBuffer對象,一旦ByteBuffer對象被垃圾回收,那么就會由ReferenceHandler線程通過通過Clean方法調用unsafe.freeMemory 是(守護線程)來釋放內存

直接內存的分配:ByteBuffer.allocateDirect();

/**
 * 禁用顯式回收對直接內存的影響
 */
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();
    }
}

這里的直接內存被釋放,不是因為GC,因為JVM管不了

但是,有虛引用

public class Code_06_DirectMemoryTest {

    public static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//        method();
        method1();
    }

    // 演示 直接內存 是被 unsafe 創建與回收
    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");//用反射拿到unsafe對象
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);

//分配內存,,用unsafe分配的內存,由unsafe對象方法釋放掉

        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB, (byte)0);
        System.in.read();

//釋放內存

        unsafe.freeMemory(base);
        System.in.read();
    }

    // 演示 直接內存被 釋放
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完畢");
        System.in.read();
        System.out.println("開始釋放");
        byteBuffer = null;
        System.gc(); // 手動 gc
        System.in.read();
    }

}

直接內存的回收不是通過 JVM 的垃圾回收來釋放的,而是通過unsafe.freeMemory 來手動釋放。
第一步:allocateDirect 的實現

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

底層是創建了一個 DirectByteBuffer 對象。
第二步:DirectByteBuffer 類

DirectByteBuffer(int cap) {   // package-private
   
    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 = Cleaner.create(this, new Deallocator(base, size, cap)); 
// 通過虛引用,來實現直接內存的釋放,this為虛引用的實際對象, 第二個參數是一個回調,實現了 runnable 接口,run 方法中通過 unsafe 釋放內存。 att = null; }

這里調用了一個 Cleaner 的 create 方法,且后台線程還會對虛引用的對象監測,如果虛引用的實際對象(這里是 DirectByteBuffer )被回收以后,就會調用 Cleaner 的 clean 方法,來清除直接內存中占用的內存。

 public void clean() {
        if (remove(this)) {
            try {
            // 都用函數的 run 方法, 釋放內存
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }

可以看到關鍵的一行代碼, this.thunk.run(),thunk 是 Runnable 對象。run 方法就是回調 Deallocator 中的 run 方法,

public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 釋放內存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

注意:

/**
     * -XX:+DisableExplicitGC 顯示的
     */
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完畢");
        System.in.read();
        System.out.println("開始釋放");
        byteBuffer = null;
        System.gc(); // 手動 gc 失效
        System.in.read();
    }

一般用 jvm 調優時,會加上下面的參數:

-XX:+DisableExplicitGC  // 靜止顯示的 GC

意思就是禁止我們手動的 GC,比如手動 System.gc() 無效,它是一種 full gc,會回收新生代、老年代,會造成程序執行的時間比較長。所以我們就通過 unsafe 對象調用 freeMemory 的方式釋放內存。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM