java方法区超详细汇总,方法区到底是干什么用的?不懂方法区不能说了解jvm!
目錄
一、運行時數據區結構圖
二、棧、堆、方法區的交互關系
三、方法區的理解
官方文檔
方法區在哪里
方法區的基本理解
HotSpot中方法區的演進
四、設置方法區大小與OOM
設置方法區內存的大小
代碼舉例1
代碼舉例2
如何解決這種OOM?
五、方法區的內部結構
方法區(Method?Area)存儲什么?
方法區的內部結構
類型信息:
域(Field)(屬性)信息:
方法(Method)信息:
non-final的類變量
全局常量(static+final)
六、運行時常量池vs常量池
常量池是什么?
為什么需要常量池??
在常量池內存儲的數據類型包括
常量池小結
運行時常量池
七、方法區使用舉例
執行順序:
八、方法區演進細節
永久代為什么要被元空間替換?
StringTable(字符串常量池)為什么要調整?
靜態變量放在哪里?
九、方法區的垃圾回收
十、總結
常見面試題
一、運行時數據區結構圖
二、棧、堆、方法區的交互關系
從線程共享與否的角度來看:
代碼角度:
從棧堆方法區內存結構來看:
三、方法區的理解
官方文檔
方法區在哪里
? ? 《Java虛擬機規范》中明確說明:“盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。”但對于HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
? ? 所以,方法區看做是一塊獨立于Java堆的內存空間。
方法區的基本理解
1.方法區(Method?Area)與Java堆一樣,是各個線程共享的內存區域。
2.方法區在JVM啟動的時候被創建,并且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的。
3.方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
4.方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類(加載大量第三方jar包、timcat部署的工程過多、大量動態生成反射類),導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤:java.lang.OutOfMemoryError: PermGen space(jdk7以前)或者java.lang.OutOfMemoryError: Metaspace(jdk8以后)
5.關閉JVM就會釋放這個區域的內存。
HotSpot中方法區的演進
1.在jdk7以前,習慣上把方法區,成為永久代。jdk8開始,使用元空間取代了永久代。
2.本質上,方法區和永久代并不等價。僅是對HotSpot而言的。《Java虛擬機規范》對如何實現方法區,不做統一要求。例如:BEA?JRockit/IBM J9中不存在永久代的概念。
? ? 現在來看,當年使用永久代,不是好的idea。導致java程序更容易OOM(超過-XX:MaxPermSize上限)
3.而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Metaspace)來代替。
4.元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代最大的區別在于:元空間不再虛擬機設置的內存中,而是使用本地內存。
5.永久代、元空間二者并不只是名字變了,內部結構也調整了。
6.根據《Java虛擬機規范》的規定,如果方法區無法滿足新的內存分配需求時,將拋出OOM異常。
四、設置方法區大小與OOM
設置方法區內存的大小
? ? 方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。
jdk7及以前:
? ? 通過-XX:PermSize來設置永久代初始分配空間。默認值是20.75M
? ? -XX:MaxPermSize來設定永久代最大可分配空間。32位機器默認是64M,64位機器默認是82M
? ? 當JVM加載的類信息容量超過了這個值,會報異常OutOfMemoryError:PermGen space。
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
如何解決這種OOM?
1.要解決OOM異常或heap?space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse?Memory?Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory?Leak)還是內存溢出(Memory?Overflow)。
2.如果是內存泄漏,可進一步通過工具查看泄漏對象到GC?Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC?Roots相關聯并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC?Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
3.如果不存在內存泄漏,換句話說就是內存中的對象卻是都還必須存活著,那就應當檢查虛擬機的堆參數(-Xmx與-Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
五、方法區的內部結構
方法區(Method?Area)存儲什么?
? ? 《深入理解Java虛擬機》書中對方法區(Method?Aera)存儲內容描述如下:它用于存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等。
方法區的內部結構
類型信息:
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區中存儲以下類型信息:
1.這個類型的完整有效名稱(全名=包名.類名)
2.這個類型直接父類的完整有效名(對于interface或是java.lang.Object,都沒有父類)
3.這個類型的修飾符(public,abstract,final的某個子集)
4.這個類型直接接口的一個有序列表
域(Field)(屬性)信息:
1.JVM必須在方法區中保存類型的所有域的相關信息以及域的聲明順序。
2.域的相關信息包括:域名稱、域類型、域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
方法(Method)信息:
JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
1.方法名稱
2.方法的返回類型(或void)
3.方法參數的數量和類型(按順序)
4.方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的某個子集)
5.方法的字節碼(bytecodes)、操作數棧、局部變量表及大小(?? ?abstract和native方法除外)
6.異常表(abstract和native方法除外)
? ? 每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引。
non-final的類變量
1.靜態變量和類關聯在一起,隨著類的加載而加載,它們成為類數據在邏輯上的一部分。
2.類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。
全局常量(static+final)
被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
六、運行時常量池vs常量池
? ? 方法區,內部包含了運行時常量池。
? ? 字節碼文件,內部包含了常量池。
? ? 要弄清楚方法區,需要理解清楚ClassFile,因為加載類的信息都在方法區。
? ? 要弄清楚方法區的運行時常量池,需要理解清楚ClassFile中?的常量池。
官方文檔:
常量池是什么?
一個有效地字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant?Pool?Table),包括各種字面量和對類型、域、和方法的符號引用。
為什么需要常量池??
? ? 一個java源文件中的類、接口,編譯后產生一個字節碼文件。而java中的字節碼需要數據支持,通常這種數據會很大以至于不能直接存到字節碼里,換另一種方式,可以存到常量池,這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池。
? ? 比如,如下代碼:
? ? 雖然很小,但是里面卻使用了String、System、PrintStream及Object等結構。這里代碼量其實已經很小了。如果代碼多,引用到的結構會更多!這里就需要常量池了!
在常量池內存儲的數據類型包括
1.數量值
2.字符串值
3.類引用
4.字段引用
5.方法引用
例如下面這段代碼:
將會被編譯成如下字節碼,其中后面的#2、#3,就指向了class文件常量池:
類似這種:
常量池小結
? ? 常量池,可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
運行時常量池
1.運行時常量池(Runtime?Constant?Pool)是方法區的一部分。
2.常量池表(Constant?Pool?Table)是Class文件的一部分,用于存放編譯器生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
3.運行時常量池,在加載類和接口到虛擬機后,就會創建對應的運行時常量池。
4.JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的(類似于用#xx來指向常量池中數據)。
5.運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析后才能獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
? ? 運行時常量池,相對于Class文件常量池的另一重要特征是:具備動態性。
?? ??? ?關于字符串String中的intern方法,是當前的字符對象(通過new出來的對象)可以使用intern方法從常量池中獲取,
?? ??? ?如果常量池中不存在該字符串,那么就新建一個這樣的字符串放到常量池中。
使用常量池的方法一個是通過雙引號定義字符串例如:String S = “1”;還有就是上面的intern方法。
6.運行時常量池類似于傳統編程語言中的符號表(symbol?table),但是它所包含的數據卻比符號表要更加豐富一些。
7.當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。
七、方法區使用舉例
javap -v -p?xxx.class,查看字節碼文件
執行順序:
八、方法區演進細節
1.首先明確:只有HotSpot才有永久代。BEA?JRockit、IBM?J9等來說,是不存在永久代的概念的。原則上如何實現方法區屬于虛擬機實現細節,不受《Java虛擬機規范》管束,并不要求統一。
2.HotSpot中方法區的變化:
| jdk1.6及以前 | 有永久代(permanent?generation),靜態變量存放在永久代上 |
| jdk1.7 | 有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量移除,保存在堆中 |
| jdk1.8及以后 | 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆 |
永久代為什么要被元空間替換?
1.隨著Java8的到來,HotSpot?VM中再也見不到永久代了。但是這并不意味著類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫做元空間(Metaspace)。
2.由于類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。
3.這項改動是很有必要的,原因有:
?? ?(1)為永久代設置空間大小是很難確定的。
? ? 在某些場景下,如果動態加載類過多,容易產生Perm區的OOM。比如某個實際web工程中,因為功能點比較多,在運行過程中,要不斷動態加載很多類,經常出現致命錯誤。
? ? 而元空間和永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。
? ? (2)對永久代進行調優是很困難的。
StringTable(字符串常量池)為什么要調整?
? ? jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full?gc的時候才會觸發。而full?gc是老年代的空間不足、永久代不足時才會觸發。這就導致StringTable回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆里,能及時回收內存。
靜態變量放在哪里?
測試一:
結論:靜態引用,對應的對象實體(new的東西)始終都存在堆空間中(不管jdk幾)。
測試二:
staticObj靜態變量隨著Test的類型信息存放在方法區,instanceObj成員變量隨著Test的對象實例存放在Java堆,localObject局部變量則是存放在foo()方法棧幀的局部變量表中。
測試發現:三個對象的數據在內存中的地址都落在Eden區范圍內,所以結論:只要是對象實例必然會在Java堆中分配。
接著,找到了一個引用該staticObj對象的地方,是在一個java.lang.Class的實例里,并且給出了這個實例的地址,通過Inspector查看該對象實例,可以清楚看到這確實是一個java.lang.Class類型的對象實例,里面有一個名為staticObj的實例字段:
? ? 從《Java虛擬機規范》所定義的概念模型來看,所有Class相關的信息都應該存放在方法區之中,但方法區該如何實現,《Java虛擬機規范》并未作出規定,這就成了一件允許不同虛擬機自己靈活把握的事情。JDK7及其以后版本的HotSpot虛擬機選擇把靜態變量與類型在Java語言一端的映射Class對象存放在一起,存儲于Java堆之中,從我們的實驗中也明確驗證了這一點。
九、方法區的垃圾回收
? ? 有些人認為方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
? ? 一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏。
? ? 方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
1.先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量: (1)類和接口的全限定名 (2)字段的名稱和描述符 (3)方法的名稱和描述符 2.HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。 3.回收廢棄常量與回收Java堆中的對象非常類似。 4.判斷一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件: (1)該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。 (2)加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。 (3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。 5.Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:TraceClassUnLoading查看類加載和卸載信息。 6.在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。十、總結
常見面試題
1.說一下JVM內存模型吧,有哪些區?分別干什么的?
2.Java8的內存分代改進
3.JVM內存分布/內存結構?棧和堆的區別?堆的結構?為什么兩個survivor區?
4.Eden和Survivor的比例分配
5.jvm內存分區,為什么要有新生代和老年代
6.講講jvm運行時數據庫區
7.什么時候對象會進入老年代?
8.JVM內存為什么要分新生代,老年代,永久代。新生代為什么要分Eden和Survivor。
9.jvm的永久代中會發生垃圾回收嗎
總結
以上是生活随笔為你收集整理的java方法区超详细汇总,方法区到底是干什么用的?不懂方法区不能说了解jvm!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 史上最全java堆,将知识点掰碎了给你嚼
- 下一篇: java垃圾回收算法超详细全解