JVM学习笔记之-方法区,栈、堆、方法区的交互关系,方法区的理解,设置方法区大小与OOM,方法区的内部结构,方法区使用举例
棧、堆、方法區的交互關系
運行時數據區結構圖
從線程共享與否的角度來看
棧,堆,方法區的交互關系
方法區的理解
方法區在哪里?
《Java虛擬機規范》中明確說明:"盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。”但對于HotSpotJVM而言,方法區還有一個別名叫做Non-Heap (非堆),目的就是要和堆分開。
所以,方法區看作是一塊獨立于Java堆的內存空間。
方法區的基本理解
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域。
方法區在JVM啟動的時候被創建,并且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的。
方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤: JDK7 為:java.lang. outOfMemoryError:PermGen space 或者JDK8為:java.lang.outofMemoryError: Metaspace
比如: 加載大量的第三方的jar包;Tomcat部署的工程過多(30-50個)﹔大量動態的生成反射類
關閉JVM就會釋放這個區域的內存。
Hotspot中方法區的演進
在jdk7及以前,習慣上把方法區,稱為永久代。jdk8開始,使用元空間取代了永久代。
本質上,方法區和永久代并不等價。僅是對hotspot而言的。《Java虛擬機規范》對如何實現方法區,不做統一要求。例如:BEA JRockit/ IBM J9中不存在永久代的概念。
現在來看,當年使用永久代,不是好的idea。導致Java程序更容易OOM(超過-XX:MaxPermsize上限)
方法區圖解
而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Metaspace)來代替
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代最大的區別在于:元空間不在虛擬機設置的內存中,而是使用本地內存。
永久代、元空間二者并不只是名字變了,內部結構也調整了。
根據《Java虛擬機規范》的規定,如果方法區無法滿足新的內存分配需求時,將拋出OOM異常。
設置方法區大小與OOM
設置方法區內存大小
方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。
jdk7及以前:
通過:-XX;PermSize來設置永久代初始分配空間。默認值是20.75M-XX:MaxPermSize來設定永久代最大可分配空間。32位機器默認是64M ,64位機器模式是82M當JTVM加載的類信息容量超過了這個值,會報異常outofMemoryError:PermGenspace 。若 執行 -XX;PermSize 提示:no such flag ‘Permsize’ 就表示當前環境大于JDK7
舉例子: -XX : PermSize=100m -XX:MaxPermSize=100m
jdk8及以后:
元數據區大小可以使用參數-XX:Metaspacesize和-XX :MaxMetaspaceSize指定,替代上述原有的兩個參數。
默認值依賴于平臺。
windows下:-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize 的值是-1,即沒有限制。與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。如果元數據區發生溢出,虛擬機一樣會拋出異常outOfMemoryError: Metaaspace
-XX:MetaspaceSize:設置初始的元空間大小。
對于一個64位的服務器端JVM來說,其默認的-XX:Metaspacesize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,Full GC將會被觸發并卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置。新的高水位線的值取決于Gc后釋放了多少元空間。
如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值。如果釋放空間過多,則適當降低該值。
如果初始化的高水位線設置過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調用。為了避免頻繁地Gc ,建議將-XX:MetaspaceSize設置為一個相對較高的值。
舉列子: -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
代碼舉例子發生OOM
列子1
列子2
代碼 JDK8環境下演示
package com.fs.demo;import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes;/*** jdk6中:* -XX :PermSize=10m -XX:MaxPermsize=10m** jdk8中:* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m** author shkstartshkstart@126.com** @create 2020 22:24*/public class OOMTest extends ClassLoader {public static void main(String[] args) {int j = 0;try {OOMTest test = new OOMTest();for (int i = 0; i < 10000; i++) {//創建cLasswriter對象,用于生成類的二進制字節碼ClassWriter classwriter = new ClassWriter(0);//classwriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "class" + i, null, "java/lang/Object", null);byte[] code = classwriter.toByteArray();//類加載test.defineClass("class" + i, code, 0, code.length);//CLass對象j++;}} finally {System.out.println(j);}} }不設置方法區參數運行
使用參數-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
運行就只有3000多個對象并且報錯OOM
如何解決這些OOM?
1、要解決ooM異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(MemoryLeak)還是內存溢出(Memory overflow)。
2、如果是內存泄漏,可進一步通過工具查看泄漏對象到Gc Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
3、如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(-xmx與-xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
方法區的內部結構
方法區(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)
·方法參數的數量和類型(按順序)
·方法的修飾符(public,private, protected, static, final,synchronized, native, abstract的一個子集)
·方法的字節碼(bytecodes)、操作數棧、局部變量表及大小(abstract和native方法除外)
·異常表( abstract和native方法除外)
每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、
被捕獲的異常類的常量池索引
代碼舉列1
代碼舉列2
non-final的類變量
·靜態變量和類關聯在一起,隨著類的加載而加載,它們成為類數據在邏輯上的一部分。
·類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。
代碼演示non-final的類變量
package com.fs.method;/*** non-final的類變量** ·靜態變量和類關聯在一起,隨著類的加載而加載,它們成為類數據在邏輯上的一部分。** ·**==類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它==**。*/ public class MethodAreaTest {public static void main(String[] args) {Order order = null;//由于類變量為靜態變量,隨著類的加載而加載,所以類變量被所有實例共享,你是類沒有具體實例,也能訪問這個類的靜態變量與方法//所以不會爆出空指針異常order.hello();System.out.println(order.count);} }class Order{//public static int count = 1;//全局常量num public static final int num = 2;public static void hello(){System.out.println("hello");} }補充說明∶全局常量 被static修飾也被final修飾了
被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
運行時常量池vs常量池
方法區,內部包含了運行時常量池。
字節碼文件,內部包含了常量池。
要弄清楚方法區,需要理解清楚classFile,因為加載類的信息都在方法區。
要弄清楚方法區的運行時常量池,需要理解清楚classFile中的常量池。https: / / docs.oracle.com/javase/specs/jvms/se8/ html/jvms-4.html。
為什么需要常量池
一個java源文件中的類、接口,編譯后產生一個字節碼文件。而Java中的字節碼需要數據支持,通常這種數據會很大以至于不能直接存到字節碼里,換另一種方式,可以存到常量池,這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池,之前有介紹。
比如:如下的代碼:
雖然只有194字節,但是里面卻使用了string、System、PrintStream及0bject等結構。這里代碼量其實已經很小了。如果代碼多,引用到的結構會更多!這里就需要常量池了!
如下圖:這些# 的都是引用的常量池的
常量池中有什么?
幾種在常量池內存儲的數據類型包括:
數量值字符串值類引用字段引用方法引用
小結:
常量池,可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
運行時常量池(Runtime constant Pool)
運行時常量池(Runtime constant Poo1)是方法區的一部分。
常量池表(Constant Pool Table)是class文件的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
運行時常量池,在加載類和接口到虛擬機后,就會創建對應的運行時常量池。
JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的。
運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
運行時常量池,相對于class文件常量池的另一重要特征是:具備動態性
運行時常量池類似于傳統編程語言中的符號表(symbol table),但是它所包含的數據卻比符號表要更加豐富一些。
當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋outofMemoryError異常。
方法區使用舉例
舉例代碼
package com.fs.method;/*** 方法區 使用舉列*/ public class MethodAreaDemo {public static void main(String[] args) {int x = 500;int y = 100;int a = x / y;int b = 50;System.out.println(a + b);} }javap -v -p MethodAreaDemo.class > test.txt
Classfile /E:/IdeaProjects/JavaEE_WEB/jvmStudy/target/classes/com/fs/method/MethodAreaDemo.classLast modified 2021-6-20; size 632 bytesMD5 checksum 9cefb2c5b00d06ff91c018fbbf56bf38Compiled from "MethodAreaDemo.java" public class com.fs.method.MethodAreaDemominor version: 0major version: 49flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #5.#24 // java/lang/Object."<init>":()V#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;#3 = Methodref #27.#28 // java/io/PrintStream.println:(I)V#4 = Class #29 // com/fs/method/MethodAreaDemo#5 = Class #30 // java/lang/Object#6 = Utf8 <init>#7 = Utf8 ()V#8 = Utf8 Code#9 = Utf8 LineNumberTable#10 = Utf8 LocalVariableTable#11 = Utf8 this#12 = Utf8 Lcom/fs/method/MethodAreaDemo;#13 = Utf8 main#14 = Utf8 ([Ljava/lang/String;)V#15 = Utf8 args#16 = Utf8 [Ljava/lang/String;#17 = Utf8 x#18 = Utf8 I#19 = Utf8 y#20 = Utf8 a#21 = Utf8 b#22 = Utf8 SourceFile#23 = Utf8 MethodAreaDemo.java#24 = NameAndType #6:#7 // "<init>":()V#25 = Class #31 // java/lang/System#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;#27 = Class #34 // java/io/PrintStream#28 = NameAndType #35:#36 // println:(I)V#29 = Utf8 com/fs/method/MethodAreaDemo#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (I)V {public com.fs.method.MethodAreaDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/fs/method/MethodAreaDemo;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=5, args_size=10: sipush 5003: istore_14: bipush 1006: istore_27: iload_18: iload_29: idiv10: istore_311: bipush 5013: istore 415: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;18: iload_319: iload 421: iadd22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V25: returnLineNumberTable:line 9: 0line 10: 4line 11: 7line 12: 11line 13: 15line 14: 25LocalVariableTable:Start Length Slot Name Signature0 26 0 args [Ljava/lang/String;4 22 1 x I7 19 2 y I11 15 3 a I15 11 4 b I } SourceFile: "MethodAreaDemo.java"圖解
方法區的演進細節
1.首先明確:只有Hotspot才有永久代。
BEA JRockit、IBM J9等來說,是不存在永久代的概念的。原則上如何實現方法區屬于虛擬機實現細節,不受《Java虛擬機規范》管束,并不要求統一。
jdk1.6及之前
有永久代(permanent generation),靜態變量存放在永久代上
jdk1.7
有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量移除,保存在堆中
jdk1.8及之后
無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆
JDK 6 方法區 - hotspot
JDK 7 方法區 - hotspot
JDK 8 方法區 - hotspot
永久代為什么要被元空間替換?
官方文檔說明為什么永久代變為元空間
隨著Java8 的到來,HotSpot VM中再也見不到永久代了。但是這并不意味著類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫做元空間(Metaspace ) 。
由于類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。
這項改動是很有必要的,原因有:
1)為永久代設置空間大小是很難確定的。
在某些場景下,如果動態加載類過多,容易產生Perm 區的OOM 。比如某個實際web工程中,因為功能點比較多,在運行過程中,要不斷動態加載很多類,經常出現致命錯誤。
而元空間和永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。
2)對永久代進行調優是很困難的。
StringTable為什么要調整?
jdk7中將stringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才會觸發。而full gc是老年代的空間不足、永久代不足時才會觸發。
這就導致stringTable回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆里,能及時回收內存。
靜態變量放在哪里
靜態引用所對應的對象始終都存放在堆空間,無論那個JDK版本都是
深入理解JAVA虛擬機 中的列子
分析工具 jdk9 bin目錄下 jhsdb.exe
上面的StaticObjectTest
方法區的垃圾回收
有些人認為方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK 11時期的zGc收集器就不支持類卸載)。
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由于低版本的Hotspot虛擬機對此區域未完全回收而導致內存泄漏。
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
1、類和接口的全限定名2、字段的名稱和描述符3、方法的名稱和描述符HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。
回收廢棄常量與回收]ava堆中的對象非常類似。
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如osGi、JSP的重加載等,否則通常是很難達成的。
該類對應的java.lang.class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceclassUnLoading查看類加載和卸載信息
在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。
總結
總結
以上是生活随笔為你收集整理的JVM学习笔记之-方法区,栈、堆、方法区的交互关系,方法区的理解,设置方法区大小与OOM,方法区的内部结构,方法区使用举例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM学习笔记之-堆,年轻代与老年代,对
- 下一篇: JVM学习笔记之-对象的实例化,内存布局