1、棧 堆 方法區的交互關系
從內存結構來看
從線程共享與否的角度來看
棧、堆、方法區的交互關系
下面就涉及了對象的訪問定位
- Person 類的 .class 信息存放在方法區中
- person 變量存放在 Java 棧的局部變量表中
- 真正的 person 對象存放在 Java 堆中
- 在 person 對象中,有個指針指向方法區中的 person 類型數據,表明這個 person 對象是用方法區中的 Person 類 new 出來的
2、方法區的理解
官方文檔: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
2.1、方法區的位置
-
《Java虛擬機規范》中明確說明:盡管所有的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。
-
但對於HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
-
所以,方法區可以看作是一塊獨立於Java堆的內存空間。
2.2、方法區的理解
方法區主要存放的是 Class,而堆中主要存放的是實例化的對象
-
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域
-
多個線程同時加載統一個類時,只能有一個線程能加載該類,其他線程只能等等待該線程加載完畢,然后直接使用該類,即類只能加載一次。
-
方法區在JVM啟動的時候被創建,並且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的。
-
方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
-
方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤:
- JDK7及以前:java.lang.OutofMemoryError:PermGen space
- 或者
- JDK8及以后:java.lang.OutOfMemoryError:Metaspace
-
舉例說明方法區 OOM。
-
加載大量的第三方的jar包
-
Tomcat部署的工程過多(30~50個)
-
大量動態的生成反射類
-
-
關閉JVM就會釋放這個區域的內存
代碼舉例
-
代碼
1 public class MethodAreaTest { 2 public static void main(String[] args) { 3 System.out.println("start..."); 4 try { 5 Thread.sleep(1000000); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 System.out.println("end..."); 10 } 11 }
-
簡單的程序,加載了好多類
2.3、方法區演進過程
Hotspot 方法區的演進過程
-
在 JDK7 及以前,習慣上把方法區,稱為永久代。JDK8開始,使用元空間取代了永久代。JDK 1.8后,元空間存放在堆外內存中
-
我們可以將方法區類比為Java中的接口,將永久代或元空間類比為Java中具體的實現類
-
本質上,方法區和永久代並不等價。僅是對Hotspot而言的可以看作等價。《Java虛擬機規范》對如何實現方法區,不做統一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
-
現在來看,當年使用永久代,不是好的idea。導致Java程序更容易OOm(超過-XX:MaxPermsize上限)
-
而到了JDK8,終於完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Metaspace)來代替
-
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機設置的內存中,而是使用本地內存
-
永久代、元空間二者並不只是名字變了,內部結構也調整了
-
根據《Java虛擬機規范》的規定,如果方法區無法滿足新的內存分配需求時,將拋出OOM異常
3、設置方法區大小與 OOM
方法區的大小不必是固定的,JVM可以根據應用的需要動態調整。
3.1、JDK7 永久代
JDK7 之前版本設置永久代大小
-
通過-XX:Permsize來設置永久代初始分配空間。默認值是20.75M
-
-XX:MaxPermsize來設定永久代最大可分配空間。32位機器默認是64M,64位機器模式是82M
-
當JVM加載的類信息容量超過了這個值,會報異常OutofMemoryError:PermGen space。
3.2、JDK8 元空間
JDK8 版本設置元空間大小
-
元數據區大小可以使用參數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
-
默認值依賴於平台,Windows下,-XX:MetaspaceSize 約為21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制。
-
與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。如果元數據區發生溢出,虛擬機一樣會拋出異常OutOfMemoryError:Metaspace
-
-XX:MetaspaceSize:設置初始的元空間大小。對於一個 64位 的服務器端 JVM 來說,其默認的 -XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,Full GC將會被觸發並卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置。新的高水位線的值取決於GC后釋放了多少元空間。
- 如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值。
- 如果釋放空間過多,則適當降低該值。
-
如果初始化的高水位線設置過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設置為一個相對較高的值。
配置元空間大小示例
-
代碼
1 /** 2 * 測試方法區默認大小 3 * 4 * JDK7及以前 5 * 設置參數:-XX:PermSize=100m -XX:MaxPermSize=100m 6 * 7 * JDK8及以后 8 * 設置參數:-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m 9 * 10 * 查看 11 * 命令:jps 12 * 命令:jinfo -flag MetaspaceSize pid 13 * 14 */ 15 public class MethodAreaTest { 16 public static void main(String[] args) { 17 System.out.println("start..."); 18 try { 19 Thread.sleep(1000000); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 System.out.println("end..."); 24 } 25 }
-
JVM 參數
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
- 命令(jps、jinfo)查看設置的元空間大小
1 MacBook-Pro-22907:test-jvm h__d$ jps 2 51988 Main 3 51846 Launcher 4 6679 5 51847 MethodAreaTest 6 52334 Jps 7 MacBook-Pro-22907:test-jvm h__d$ jinfo -flag MetaspaceSize 51847 8 -XX:MetaspaceSize=104857600
3.3、方法區 OOM
方法區 OOM 舉例
- 代碼:OOMTest 類繼承 ClassLoader 類,獲得 defineClass() 方法,可自己進行類的加載
1 /** 2 * JDK7及以前 3 * 設置參數:-XX:PermSize=100m -XX:MaxPermSize=100m 4 * 5 * JDK8及以后 6 * 設置參數:-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m 7 * 8 * 示例:前提設置方法區大小,不停的加載類,造成方法區OOM 9 */ 10 public class OOMTest extends ClassLoader{ 11 public static void main(String[] args) { 12 int j = 0; 13 try { 14 OOMTest test = new OOMTest(); 15 for (int i = 0; i < 10000; i++) { 16 //創建ClassWriter對象,用於生成類的二進制字節碼 17 ClassWriter classWriter = new ClassWriter(0); 18 //指明版本號,修飾符,類名,包名,父類,接口 19 classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); 20 //返回byte[] 21 byte[] code = classWriter.toByteArray(); 22 //類的加載 23 test.defineClass("Class" + i, code, 0, code.length);//Class對象 24 j++; 25 } 26 } finally { 27 System.out.println(j); 28 } 29 } 30 }
-
運行參數:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
- 運行結果
3.4、解決 OOM
如何解決 OOM?
-
要解決OOM異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Ec1ipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)
-
內存泄漏就是有大量的引用指向某些對象,但是這些對象以后不會使用了,但是因為它們還和GC ROOT有關聯,所以導致以后這些對象也不會被回收,這就是內存泄漏的問題
-
如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。於是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較准確地定位出泄漏代碼的位置。
-
如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx與-Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
4、方法區的內部結構
4.1、方法區結構
方法區(Method Area)存儲什么?
《深入理解Java虛擬機》書中對方法區(Method Area)存儲內容描述如下:
它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等。
類型信息
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區中存儲以下類型信息:
-
這個類型的完整有效名稱(全名=包名.類名)
-
這個類型直接父類的完整有效名(對於interface或是java.lang.Object,都沒有父類)
-
這個類型的修飾符(public,abstract,final的某個子集)
-
這個類型直接接口的一個有序列表
域(Field)信息
-
JVM必須在方法區中保存類型的所有域的相關信息以及域的聲明順序。
-
域的相關信息包括:
-
域名稱
-
域類型
-
域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
-
方法(Method)信息
JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
-
方法名稱
-
方法的返回類型(包括 void 返回類型),void 在 Java 中對應的類為 void.class
-
方法參數的數量和類型(按順序)
-
方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
-
方法的字節碼(bytecodes)、操作數棧、局部變量表及大小(abstract和native方法除外)
-
異常表(abstract和native方法除外),異常表記錄每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引
說明:
-
descriptor: ()V 表示方法返回值類型為 void
-
flags: ACC_PUBLIC 表示方法權限修飾符為 public
-
stack=3 表示操作數棧深度為 3
-
locals=2 表示局部變量個數為 2 個(實力方法包含 this)
-
test1() 方法雖然沒有參數,但是其 args_size=1 ,這時因為將 this 作為了參數
代碼示例
-
代碼
1 /** 2 * 測試方法區的內部構成 3 */ 4 public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable { 5 //屬性 6 public int num = 10; 7 private static String str = "測試方法的內部結構"; 8 9 //構造器沒寫 10 11 //方法 12 public void test1() { 13 int count = 20; 14 System.out.println("count = " + count); 15 } 16 17 public static int test2(int cal) { 18 int result = 0; 19 try { 20 int value = 30; 21 result = value / cal; 22 } catch (Exception e) { 23 e.printStackTrace(); 24 } 25 return result; 26 } 27 28 @Override 29 public int compareTo(String o) { 30 return 0; 31 } 32 }
-
反編譯字節碼文件,並輸出值文本文件中,便於查看
-
參數 -p 確保能查看 private 權限類型的字段或方法
javap -v -p MethodInnerStrucTest.class > Text.txt
4.2、域信息特殊情況
non-final 類型的類變量
-
靜態變量和類關聯在一起,隨着類的加載而加載,他們成為類數據在邏輯上的一部分
-
類變量被類的所有實例共享,即使沒有類實例時,你也可以訪問它
代碼示例
-
如下代碼所示,即使我們把order設置為null,也不會出現空指針異常
-
這更加表明了 static 類型的字段和方法隨着類的加載而加載,並不屬於特定的類實例
1 /** 2 * non-final的類變量 3 */ 4 public class StaticVariableTest { 5 public static void main(String[] args) { 6 Order order = null; 7 order.hello(); 8 System.out.println(order.count); 9 } 10 } 11 12 class Order { 13 public static int count = 1; 14 public static final int number = 2; 15 16 17 public static void hello() { 18 System.out.println("hello!"); 19 } 20 }
-
運行結果,未報錯
全局常量:static final
-
全局常量就是使用 static final 進行修飾
-
被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
- 反編譯上面字節碼文件Order.class,查看字節碼指令,可以發現 number 的值已經寫死在字節碼文件中了
命令:javap -v Order.class
4.3、運行時常量池
運行時常量池 VS 常量池
官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
-
方法區,內部包含了運行時常量池
-
字節碼文件,內部包含了常量池
-
要弄清楚方法區,需要理解清楚ClassFile,因為加載類的信息都在方法區。
-
要弄清楚方法區的運行時常量池,需要理解清楚ClassFile中的常量池。
常量池
-
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外
-
還包含一項信息就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符號引用
為什么需要常量池?
-
一個java源文件中的類、接口,編譯后產生一個字節碼文件。而Java中的字節碼需要數據支持,通常這種數據會很大以至於不能直接存到字節碼里,換另一種方式,可以存到常量池
-
這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池,之前有介紹
比如:如下的代碼:
1 public class SimpleClass { 2 public void sayHello() { 3 System.out.println("hello"); 4 } 5 }
-
雖然上述代碼只有194字節,但是里面卻使用了String、System、PrintStream及Object等結構。
-
如果不使用常量池,就需要將用到的類信息、方法信息等記錄在當前的字節碼文件中,造成文件臃腫
-
所以我們將所需用到的結構信息記錄在常量池中,並通過引用的方式,來加載、調用所需的結構
-
這里的代碼量其實很少了,如果代碼多的話,引用的結構將會更多,這里就需要用到常量池了。
常量池中有啥?
-
數量值
-
字符串值
-
類引用
-
字段引用
-
方法引用
常量池代碼舉例
-
代碼
1 public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable { 2 //屬性 3 public int num = 10; 4 private static String str = "測試方法的內部結構"; 5 6 //構造器沒寫 7 8 //方法 9 public void test1() { 10 int count = 20; 11 System.out.println("count = " + count); 12 } 13 14 public static int test2(int cal) { 15 int result = 0; 16 try { 17 int value = 30; 18 result = value / cal; 19 } catch (Exception e) { 20 e.printStackTrace(); 21 } 22 return result; 23 } 24 25 @Override 26 public int compareTo(String o) { 27 return 0; 28 } 29 }
- javap反編譯之后
常量池總結
常量池、可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型
運行時常量池
-
運行時常量池(Runtime Constant Pool)是方法區的一部分。
-
常量池表(Constant Pool Table)是Class字節碼文件的一部分,用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
-
運行時常量池,在加載類和接口到虛擬機后,就會創建對應的運行時常量池。
-
JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的。
-
運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
-
運行時常量池,相對於Class文件常量池的另一重要特征是:具備動態性。
-
運行時常量池類似於傳統編程語言中的符號表(symbol table),但是它所包含的數據卻比符號表要更加豐富一些。
-
當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutofMemoryError異常。
6、方法區演進細節
6.1、永久代演進過程
關於永久代的說明
-
首先明確:只有Hotspot才有永久代。
-
BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,不受《Java虛擬機規范》管束,並不要求統一
-
Hotspot中方法區的變化:
JDK 版本 演變細節 JDK1.6及以前 有永久代(permanent generation),靜態變量存儲在永久代上 JDK1.7 有永久代,但已經逐步 “去永久代”,字符串常量池,靜態變量移除,保存在堆中 JDK1.8 無永久代,類型信息,字段,方法,常量保存在本地內存的元空間,但字符串常量池、靜態變量仍然在堆中。
JDK 6 方法區
JDK 7 方法區
JDK 8 方法區
6.2、元空間出現原因
永久代為什么要被元空間替代?
官方文檔: http://openjdk.java.net/jeps/122
-
官方的牽強解釋:JRockit是和HotSpot融合后的結果,因為JRockit沒有永久代,所以他們不需要配置永久代
-
隨着Java8的到來,HotSpot VM中再也見不到永久代了。但是這並不意味着類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫做元空間(Metaspace)。
由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間,這項改動是很有必要的,原因有:
-
為永久代設置空間大小是很難確定的。
- 在某些場景下,如果動態加載類過多,容易產生Perm區的OOM。比如某個實際Web工
程中,因為功能點比較多,在運行過程中,要不斷動態加載很多類,經常出現致命錯誤。Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space
- 而元空間和永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。
因此,默認情況下,元空間的大小僅受本地內存限制。
- 在某些場景下,如果動態加載類過多,容易產生Perm區的OOM。比如某個實際Web工
-
對永久代進行調優是很困難的。
-
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再用的類型,方法區的調優主要是為了降低Full GC
-
有些人認為方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
-
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由於低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏
-
6.3、字符串常量池
字符串常量池 StringTable 為什么要調整位置?
-
JDK7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在Full GC的時候才會執行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時才會觸發。
-
這就導致StringTable回收效率不高,而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆里,能及時回收內存。
6.4、靜態變量位置
- 從《Java虛擬機規范》所定義的概念模型來看,所有Class相關的信息都應該存放在方法區之中,但方法區該如何實現,《Java虛擬機規范》並未做出規定,這就成了一件允許不同虛擬機自己靈活把握的事情。JDK7及其以后版本的HotSpot虛擬機選擇把靜態變量與類型在Java語言一端的映射Class對象存放在一起,存儲於Java堆之中,從我們的實驗中也明確驗證了這一點
參考:https://blog.csdn.net/oneby1314/article/details/108040357
7、方法區的垃圾回收
方法區垃圾收集
-
有些人認為方法區(如Hotspot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。
-
《Java虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
-
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由於低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏。
-
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
方法區常量的回收
-
先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用
- 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等
- 而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
-
HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。
-
回收廢棄常量與回收Java堆中的對象非常類似。(關於常量的回收比較簡單,重點是類的回收)
方法區類的回收
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。關於是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc
參數進行控制,還可以使用-verbose:class
以及 -XX:+TraceClass-Loading
、-XX:+TraceClassUnLoading
查看類加載和卸載信息
在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。