一、前言
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 的方式釋放內存。