JVM入門之內(nèi)存結(jié)構(gòu)(堆、方法區(qū))
1、堆
1.1 定義
- 是Java內(nèi)存區(qū)域中一塊用來存放對象實例的區(qū)域【
幾乎所有的對象實例都在這里分配內(nèi)存
】 - 通過
new
關(guān)鍵字創(chuàng)建的對象都會被放在堆內(nèi)存,jvm 運行時數(shù)據(jù)區(qū)中,占用內(nèi)存最大的就是堆(Heap)內(nèi)存!
1.2 堆的作用
- 此內(nèi)存區(qū)域的唯一目的就是存放對象實例
- 方法體中的引用變量和基本類型的變量都在棧上,其他都在堆上
- Java 堆(Java Heap)是 Java 虛擬機所管理的內(nèi)存中最大的一塊 Java
堆是被所有線程
共享的一塊內(nèi)存區(qū)域
1.3 特點
- 所有線程共享,堆內(nèi)存中的對象都需要考慮線程安全問題
- 有垃圾回收機制,Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做“GC 堆”(Garbage)
- Java堆可以分成新生代和老年代 新生代可分為To Space、From Space、Eden
-Xmx -Xms
:JVM初始分配的堆內(nèi)存由-Xms指定,默認是物理內(nèi)存的1/64
1.4 堆內(nèi)存溢出
java.lang.OutofMemoryError :java heap space.
堆內(nèi)存溢出。
內(nèi)存溢出案例:
/** * 演示堆內(nèi)存溢出 java.lang.OutOfMemoryError: Java heap space * -Xmx8m 最大堆空間的jvm虛擬機參數(shù),默認是4g */ public class Demo05 { public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>();// new 一個list 存入堆中-------- list的有效范圍 --------- String a = "hello"; while (true) {// 不斷地向list 中添加 a list.add(a); // hello, hellohello, hellohellohellohello ... a = a + a; // hellohellohellohello i++; }//------------------------------------------------------------------ list的有效范圍 --------- } catch (Throwable e) {// list 使用結(jié)束,被jc 垃圾回收 e.printStackTrace(); System.out.println(i); } } }
異常輸出結(jié)果:
// 給list分配堆內(nèi)存后,while(true)不斷向其中添加a 最終堆內(nèi)存溢出 java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) at java.lang.StringBuilder.append(StringBuilder.java:136) at com.haust.jvm_study.demo.Demo05.main(Demo05.java:19)
1.5 堆內(nèi)存診斷
用于堆內(nèi)存診斷的工具:
- jps
- jmap
- jconsole
- jvirsalvm
2、方法區(qū)
方法區(qū)概述:
- 方法區(qū)在JVM啟動的時候被創(chuàng)建,并且它的實際的物理內(nèi)存空間和Java堆區(qū)一樣都可以是不連續(xù)的, 關(guān)閉Jvm就會釋放這個區(qū)域的內(nèi)存。
- 方法區(qū)邏輯上是堆的一個組成部分,但是在不同版本的虛擬機里實現(xiàn)是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metaspace)
- (注意:方法區(qū)時一種規(guī)范,而永久代和元空間是它的一種實現(xiàn)方式)
- 方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤:(
java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。
- 方法區(qū)用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。
- 類型信息:( 類class、接口interface、枚舉enum、注解annotation)JVM必須在方法區(qū)中存儲以下類型信息:
- 這個類型的完整有效名稱(全名=包名.類名)
- 這個類型直接父類的完整有效名(對于interface或是java. lang.Object,都沒有父類)
- 這個類型的修飾符(public, abstract, final的某個子集)
- 這個類型直接接口的一個有序列表
- 域信息(成員變量):
- JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序。
- 域的相關(guān)信息包括:域名稱、 域類型、域修飾符(public, private, protected, static, final, volatile, transient的某個子集)。
- 方法信息:JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序
- 方法名稱
- 方法的返回類型(或void)
- 方法參數(shù)的數(shù)量和類型(按順序)
- 方法的修飾符(public, private, protected, static, final,synchronized, native , abstract的一個子集)
- 方法的字節(jié)碼(bytecodes)、操作數(shù)棧、局部變量表及大?。?abstract和native 方法除外)異常表( abstract和native方法除外)
- 每個異常處理的開始位置、結(jié)束位置、代碼處理在程序計數(shù)器中的偏移地址、被捕獲的異常類的常量池索引
2.1 結(jié)構(gòu)(1.6 對比 1.8)
由上圖可以看出,1.6版本方法區(qū)是由PermGen永久代實現(xiàn)(使用堆內(nèi)存的一部分作為方法區(qū)),且由JVM 管理,由Class ClassLoader 常量池(包括StringTable) 組成。
1.8 版本后,方法區(qū)交給本地內(nèi)存管理,而脫離了JVM,由元空間實現(xiàn)(元空間不再使用堆的內(nèi)存,而是使用本地內(nèi)存,即操作系統(tǒng)的內(nèi)存),由Class ClassLoader 常量池(StringTable 被移到了Heap 堆中管理) 組成。
- 方法區(qū)是什么?
- 是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息(比如class文件)、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
- 什么是類信息:類版本號、方法、接口。
- 方法區(qū)作用
- 內(nèi)存中存放類信息、靜態(tài)變量、常量等數(shù)據(jù),屬于線程共享的一塊區(qū)域。
- Hotspot使用永久代來實現(xiàn)方法區(qū) JRockit、IBM J9VM Java堆一樣管理這部分內(nèi)存。
- 方法區(qū)特點
- 并非數(shù)據(jù)進入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載。
- 方法區(qū)也會拋出OutofMemoryError,當它無法滿足內(nèi)存分配需求時 。
方法區(qū)的演進:
面試常問
- Jdk 1.6 及之前:有永久代(靜態(tài)變量存放在永久代上)、字符串常量池(1.6在方法區(qū))
- Jdk 1.7 :有永久代,但已經(jīng)逐步 " 去永久代 ",字符串常量池、靜態(tài)變量移除,保存在堆中
- dk 1.8 及之后: 無永久代,常量池1.8在元空間。但靜態(tài)變量、字符串常量池仍在堆中
為什么要用元空間取代永久代?
永久代設(shè)置空間大小很難確定:(
①. 永久代參數(shù)設(shè)置過小,在某些場景下,如果動態(tài)加載的類過多,容易產(chǎn)生Perm區(qū)的OOM,比如某個實際Web工程中,因為功能點比較多,在運行過程中,要不斷動態(tài)加載很多類,經(jīng)常出現(xiàn)致命錯誤
②. 永久代參數(shù)設(shè)置過大,導致空間浪費
③. 默認情況下,元空間的大小受本地內(nèi)存限制)
永久代進行調(diào)優(yōu)很困難:(方法區(qū)的垃圾收集主要回收兩部分:常量池中廢棄的常量和不再使用的類型,而不再使用的類或類的加載器回收比較復雜,full gc 的時間長)
StringTable為什么要調(diào)整?
- jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才能觸發(fā)。而full gc是老年代的空間不足、永久代不足才會觸發(fā)。
- 這就導致StringTable回收效率不高,而我們開發(fā)中會有大量的字符串被創(chuàng)建,回收效率低,導致永久代內(nèi)存不足,放到堆里,能及時回收內(nèi)存。
設(shè)置方法區(qū)大小
jdk7及以前:
-XX:PermSize=100m
(默認值是20.75M)-XX:MaxPermSize=100m
(32位機器默認是64M,64位機器模式是82M)
jdk1.8及以后:
-XX:MetaspaceSize=100m
(windows下,默認約等于21M)- -
XX:MaxMetaspaceSize=100m
(默認是-1,即沒有限制)
2.2 內(nèi)存溢出
1.8以前會導致永久代
內(nèi)存溢出
1.8以后會導致元空間
內(nèi)存溢出
案例
調(diào)整虛擬機參數(shù):-XX:MaxMetaspaceSize=8m
/** * 演示元空間內(nèi)存溢出 java.lang.OutOfMemoryError: Metaspace * -XX:MaxMetaspaceSize=8m */ public class Demo1_8 extends ClassLoader { // 可以用來加載類的二進制字節(jié)碼 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 作用是生成類的二進制字節(jié)碼 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(); // 執(zhí)行了類的加載 test.defineClass("Class" + i, code, 0, code.length); // Class 對象 } } finally { System.out.println(j); } } }
3331 Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space // 元空間內(nèi)存溢出 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.lang.ClassLoader.defineClass(ClassLoader.java:642) at com.haust.jvm_study.metaspace.Demo1_8.main(Demo1_8.java:23)
2.3 常量池
常量池,可以看做是一張表,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名,方法名,參數(shù)類型、字面量等信息
類的二進制字節(jié)碼的組成:類的基本信息、常量池、類的方法定義(包含了虛擬機指令)。
通過反編譯來查看類的信息:
- 獲得對應類的
.class
文件 - 在JDK對應的bin目錄下運行cmd,也可以在IDEA控制臺輸入
- 輸入 javac 對應類的絕對路徑
F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy
輸入完成后,對應的目錄下就會出現(xiàn)類的.class
文件
在控制臺輸入javap -v 類的絕對路徑
javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
然后能在控制臺看到反編譯以后類的信息了
- 類的基本信息
- 常量池
- 虛擬機中執(zhí)行編譯的方法(框內(nèi)的是真正編譯執(zhí)行的內(nèi)容,#號的內(nèi)容需要在常量池中查找)
2.4 運行時常量池
- 常量池
- 就是一張表(如上圖中的constant pool),虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量信息
- 運行時常量池
- 常量池是*.class文件中的,當該類被加載以后,它的常量池信息就會放入運行時常量池,并把里面的符號地址變?yōu)檎鎸崈?nèi)存地址
- 行時常量池( Runtime Constant Pool)是方法區(qū)的一部分。
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
- 運行時常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
- **(方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號引用。**字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
java 8 后,永久代已經(jīng)被移除,被稱為“元數(shù)據(jù)區(qū)”的區(qū)域所取代。類的元數(shù)據(jù)放入native memory, 字符串池和類的靜態(tài)變量放入java堆中(靜態(tài)變量之前是放在方法區(qū))。
2.5 常量池與串池的關(guān)系
串池StringTable
- 常量池是.class文件,存放堆中數(shù)據(jù)的引用地址,而不是真實的對象,運行時常量池是jvm運行時將常量池中數(shù)據(jù)放入池中,此時引用地址真正的指向?qū)ο蠖皇?class文件;Stringtable是哈希表(不能擴容),它也叫做串池,用來存儲字符串,這3個不是同一個東西,我們需要進行區(qū)分。
- StringTable中存儲的并不是String類型的對象,存儲的而是指向String對象的索引,真實對象還是存儲在堆中
- jdk1.6中,StringTable是放在永久代(方法區(qū))中,jvm進行FullGC才會對常量池進行垃圾回收,影響效率,因此在jdk1.8中將StringTable放在堆中,jvm內(nèi)存緊張時就會對StringTable進行垃圾回收。
特征
- 常量池中的字符串僅是符號,只有在被用到時才會轉(zhuǎn)化為對象
- 利用串池的機制,來避免重復創(chuàng)建字符串對象
- 字符串變量拼接的原理是StringBuilder
- 字符串常量拼接的原理是編譯器優(yōu)化
- 可以使用intern方法,主動將串池中還沒有的字符串對象放入串池中
注意:無論是串池還是堆里面的字符串,都是對象
串池作用:用來放字符串對象且里面的元素不重復
public class StringTableStudy { public static void main(String[] args) { String a = "a"; String b = "b"; String ab = "ab"; } }
常量池中的信息,都會被加載到運行時常量池中,但這是a b ab 僅是常量池中的符號,還沒有成為java字符串
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: returnCopy
當執(zhí)行到 ldc #2
時,會把符號 a 變?yōu)?code>“a”字符串對象,并放入串池中(hashtable結(jié)構(gòu) 不可擴容)
當執(zhí)行到ldc #3
時,會把符號 b 變?yōu)?code>“b” 字符串對象,并放入串池中。
當執(zhí)行到ldc #4
時,會把符號 ab 變?yōu)?code>“ab”字符串對象,并放入串池中。
最終串池中存放:StringTable [“a”, “b”, “ab”
注意:字符串對象的創(chuàng)建都是懶惰的,只有當運行到那一行字符串且在串池中不存在的時候(如 ldc #2
)時,該字符串才會被創(chuàng)建并放入串池中。
案例1:使用拼接字符串變量對象創(chuàng)建字符串的過程:
public class StringTableStudy { public static void main(String[] args) { String a = "a"; String b = "b"; String ab = "ab"; // 拼接字符串對象來創(chuàng)建新的字符串 String ab2 = a+b; // StringBuilder().append(“a”).append(“b”).toString() } }
反編譯后的結(jié)果:
Code: stack=2, locals=5, 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/Str ing; 27: astore 4 29: return
通過拼接的方式來創(chuàng)建字符串的過程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString()
方法的返回值是一個新的字符串對象,但字符串的值和拼接的字符串一致,但是兩個不同的字符串,一個存在于串池之中,一個存在于堆內(nèi)存之中
String ab = "ab";// 串池之中 String ab2 = a+b;// 堆內(nèi)存之中 // 結(jié)果為false,因為ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一個對象,存在于堆內(nèi)存之中 System.out.println(ab == ab2);
案例2:使用拼接字符串常量對象的方法創(chuàng)建字符串
public class StringTableStudy { public static void main(String[] args) { String a = "a"; String b = "b"; String ab = "ab"; // 拼接字符串對象來創(chuàng)建新的字符串 String ab2 = a+b;// StringBuilder().append(“a”).append(“b”).toString() // 使用拼接字符串的方法創(chuàng)建字符串 String ab3 = "a" + "b";// String ab (javac 在編譯期進行了優(yōu)化) } }
反編譯后的結(jié)果:
Code: stack=2, 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/Str ing; 27: astore 4 // ab3初始化時直接從串池中獲取字符串 29: ldc #4 // String ab 31: astore 5 33: return
- 使用拼接字符串常量的方法來創(chuàng)建新的字符串時,因為內(nèi)容是常量,javac在編譯期會進行優(yōu)化,結(jié)果已在編譯期確定為ab,而創(chuàng)建ab的時候已經(jīng)在串池中放入了
“ab”,
所以ab3直接從串池中獲取值,所以進行的操作和ab = “ab”
一致。 - 使用
拼接字符串變量
的方法來創(chuàng)建新的字符串時,因為內(nèi)容是變量,只能在運行期確定它的值,所以需要使用StringBuffer來創(chuàng)建
JDK1.8 中的intern方法
調(diào)用字符串對象的intern
方法,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象,則放入成功.
- 如果有該字符串對象,則放入失敗.
無論放入是否成功,都會返回串池中的字符串對象.
注意:此時如果調(diào)用intern方法成功,堆內(nèi)存與串池中的字符串對象是同一個對象;如果失敗,則不是同一個對象!
例1
public class Main { public static void main(String[] args) { // "a" "b" 被放入串池中,str則存在于堆內(nèi)存之中 String str = new String("a") + new String("b"); // 調(diào)用str的intern方法,這時串池中如果沒有"ab",則會將該字符串對象放入到串池中,放入成功~此時堆內(nèi)存與串池中的"ab"是同一個對象 String st2 = str.intern(); // 給str3賦值,因為此時串池中已有"ab",則直接將串池中的內(nèi)容返回 String str3 = "ab"; // 因為堆內(nèi)存與串池中的"ab"是同一個對象,所以以下兩條語句打印的都為true System.out.println(str == st2);// true System.out.println(str == str3);// true } }
例2
public class Main { public static void main(String[] args) { // 此處創(chuàng)建字符串對象"ab",因為串池中還沒有"ab",所以將其放入串池中 String str3 = "ab"; // "a" "b" 被放入串池中,str則存在于堆內(nèi)存之中 String str = new String("a") + new String("b"); // 此時因為在創(chuàng)建str3時,"ab"已存在與串池中,所以放入失敗,但是會返回**串池**中的"ab" String str2 = str.intern(); System.out.println(str == str2);// false System.out.println(str == str3);// false System.out.println(str2 == str3);// true } }
JDK1.6 中的intern
方法
調(diào)用字符串對象的intern方法,會將該字符串對象嘗試放入到串池中
如果串池中沒有該字符串對象,會將該字符串對象復制一份,再放入到串池中如果有該字符串對象,則放入失敗
無論放入是否成功,都會返回串池中的字符串對象
注意:此時無論調(diào)用intern方法成功與否,串池中的字符串對象和堆內(nèi)存中的字符串對象都不是同一個對象
2.6 StringTable的位置
如圖:
- JDK1.6 時,StringTable是屬于常量池的一部分。
- JDK1.8 以后,StringTable是放在堆中的。
2.7 StringTable 垃圾回收
StringTable在內(nèi)存緊張時,會發(fā)生垃圾回收。
2.8 方法區(qū)的垃圾回收
(1).有些人認為方法區(qū)(如Hotspot,虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如 JDK11 時期的 ZGC 收集器就不支持類卸載)
(2). 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。以前 Sun 公司的 Bug 列表中,曾出現(xiàn)過的若干個嚴重的 Bug 就是由于低版本的 Hotspot 虛擬機對此區(qū)域未完全回收而導致內(nèi)存泄漏。
- 法區(qū)的垃圾收集主要回收兩部分內(nèi)容:
常量池中廢奔的常量和不再使用的類型
- 先來說說方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號引用。 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
- HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收?;厥諒U棄常量與回收Java堆中的對象非常類似。
- 判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設(shè)計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的
- 該類對應的
java.lang.Class
對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法 - Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關(guān)于是否要對類型進行回收,HotSpot虛擬機提供了一Xnoclassgc 參數(shù)進行控制,還可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查看類加載和卸載信息。
- 在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架,動態(tài)生成JSP以及oSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區(qū)造成過大的內(nèi)存壓力。
3、直接內(nèi)存
- 屬于操作系統(tǒng),常見于NIO操作時,用于數(shù)據(jù)緩沖區(qū)
- 分配回收成本較高,但讀寫性能高
- 不受JVM內(nèi)存回收管理 文件讀寫流程
使用了DirectBuffer
直接內(nèi)存是操作系統(tǒng)和Java代碼都可以訪問的一塊區(qū)域,無需將代碼從系統(tǒng)內(nèi)存復制到Java堆內(nèi)存,從而提高了效率。
釋放原理
直接內(nèi)存的回收不是通過JVM的垃圾回收來釋放的,而是通過unsafe.freeMemory來手動釋放。
//通過ByteBuffer申請1M的直接內(nèi)存 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
申請直接內(nèi)存,但JVM并不能回收直接內(nèi)存中的內(nèi)容,它是如何實現(xiàn)回收的呢?
allocateDirect的實現(xiàn):
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }Copy
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); //申請內(nèi)存 } 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)); //通過虛引用,來實現(xiàn)直接內(nèi)存的釋放,this為虛引用的實際對象 att = null; }
這里調(diào)用了一個Cleaner的create方法,且后臺線程還會對虛引用的對象監(jiān)測,如果虛引用的實際對象(這里是DirectByteBuffer)被回收以后,就會調(diào)用Cleaner的clean方法,來清除直接內(nèi)存中占用的內(nèi)存。
public void clean() { if (remove(this)) { try { this.thunk.run(); //調(diào)用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; } }); }
對應對象的run方法:
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); //釋放直接內(nèi)存中占用的內(nèi)存 address = 0; Bits.unreserveMemory(size, capacity); }
直接內(nèi)存的回收機制總結(jié)
- 使用了Unsafe類來完成直接內(nèi)存的分配回收,回收需要主動調(diào)用freeMemory方法。
- ByteBuffer的實現(xiàn)內(nèi)部使用了Cleaner(虛引用)來檢測ByteBuffer。一旦ByteBuffer被垃圾回收,那么會由ReferenceHandler來調(diào)用Cleaner的clean方法調(diào)用freeMemory來釋放內(nèi)存。ByteBuffer被垃圾回收,那么會由ReferenceHandler來調(diào)用Cleaner的clean方法調(diào)用freeMemory來釋放內(nèi)存。
希望大家可以多多關(guān)注腳本之家的其他文章!
相關(guān)文章
使用Java獲取html中Select,radio多選的值方法
以下是對使用Java獲取html中Select,radio多選值的方法進行了詳細的分析介紹,需要的朋友可以過來參考下2013-08-08JDBC實現(xiàn)數(shù)據(jù)庫增刪改查功能
這篇文章主要為大家詳細介紹了JDBC實現(xiàn)數(shù)據(jù)庫增刪改查功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07Java零基礎(chǔ)也看得懂的單例模式與final及抽象類和接口詳解
本文主要講了單例模式中的餓漢式和懶漢式的區(qū)別,final的使用,抽象類的介紹以及接口的具體內(nèi)容,感興趣的朋友來看看吧2022-05-05springboot連接不同數(shù)據(jù)庫的寫法詳解
這篇文章主要介紹了springboot連接不同數(shù)據(jù)庫的寫法?,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04Springboot?中的?Filter?實現(xiàn)超大響應?JSON?數(shù)據(jù)壓縮的方法
這篇文章主要介紹了Springboot?中的?Filter?實現(xiàn)超大響應?JSON?數(shù)據(jù)壓縮,定義GzipFilter對輸出進行攔截,定義 Controller該 Controller 非常簡單,主要讀取一個大文本文件,作為輸出的內(nèi)容,本文通過實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2022-10-10