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