JVM詳解(九)方法區
文章目錄
- JVM詳解(九)方法區
- 在這裡感謝尚矽谷JVM(宋紅康),在此記錄一下自己詳細對學習筆記,希望對你有所幫助。
-
-
- 一、方法區概述——堆棧方法區間的互動關系
- 二、方法區的了解
- 三、Hotspot中方法區的演進
- 四、設定方法區大小的參數
- 五、OOM:PermGen和OOM:Metaspace
- 六、方法區内部結構
-
- 第一部分:
- 第二部分:
- 七、class檔案中常量池的了解
- 八、運作時常量池的了解
- 九、圖示舉例方法區的使用
- 十、方法區在jdk6、jdk7、jdk8中的演進細節
- 十一、StringTable為什麼要調整位置
- 十二、如何證明靜态變量存在哪
- 十三、方法區的垃圾回收行為
- 十四、運作時資料區的總結與常見大腸面試題說明
-
在這裡感謝尚矽谷JVM(宋紅康),在此記錄一下自己詳細對學習筆記,希望對你有所幫助。
視訊位址
代碼位址
一、方法區概述——堆棧方法區間的互動關系
方法區也是除堆空間之外非常重要的結構

從記憶體結構上來講,這就是一個運作時資料區中的完整結構
我們要了解平常這些反複是怎麼配置設定到棧、堆、方法區的,他們三者是如何配合的
由于堆和元空間是線程共享的,他們通信更友善,但是要考慮到線程安全的問題,尤其是在這個堆空間。元空間除了你動态加載的這個時候,一般情況下是比較穩定的,是以它的gc不會像堆空間那樣頻繁。
至于右邊他們要報錯就是StackOverFlow,因為他們都是這個棧的結構。程式計數器它不會爆異常,也不存在gc。虛拟機站跟本地方法棧沒有GC。左邊都有。
如何保證多個線程下并發下的安全性,我們就會用到ThreadLocal。這個ThreadLocal可以單獨去了解一下
Person這個類的類型很明顯我們會将它加載到這個方法區【關于類本身,這個.class檔案,運作時類本身我們要放在方法區】。
new的這個對象要放在堆空間中。
如果這個Person person = new Person()是在一個方法中寫的,這個person就是一個局部變量,每一個線程都是一個棧桢。去體會下上面的圖
二、方法區的了解
在此貼上官方文檔位址
《Java,虛拟機規範》中明确說明:“盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實作可能不會選擇去進行垃圾收集或者進行壓縮。”但對 于HotSpotJVM而言,方法區還有一個别名叫做Non- -Heap (非堆),目的就是要和堆分開。
是以,方法區看作是一塊獨立于Java堆的記憶體空間。
Metaspace就是方法區的落地實作,沒有算他。是以大家可以這樣去了解把它想做是獨立的一個結構
- 方法區(MethodArea)與Java堆一樣,是各個線程共享的記憶體區域。【意思是竟然是一個共享區域,如果這個類沒有加載的話,隻能有一個線程去調用ClassLoader,其他線程想要使用這個類的話就必須得等待。即我們隻需要加載一次】
- 方法區在JVM啟動的時候被建立,并且它的實際的實體記憶體空間中和Java堆區–樣都可以是不連續的。
-
方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充。方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導緻方法區溢出,虛拟機同樣會抛出記憶體溢出錯誤: java. lang .OutofMemoryError:
PermGen space 或者java. lang.OutOfMemoryError: Metaspace
- 關閉JVM就會釋放這個區域的記憶體。
雖然這個代碼簡介,但是跑起來的時候是有很多類需要加載的,我們可以來看一下。
如果類加載過多,即超過方法區的大小,就會報OOM。如加載大量第三方的jar包。或者Tomcat部署的工程過多(30-50個)。大量動态的生成反射類。
三、Hotspot中方法區的演進
- 在jdk7及以前,習慣上把方法區,稱為永久代。jdk8開始, 使用元空間取代了永久代。【可以把方法區當成接口,永久帶或者元空間當作實作】
-
本質上,方法區和永久代并不等價。僅是對hotspot而言的。《Java 虛拟規範》對如何實作方法區,不做統一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。
➢現在來看,當年使用永久代,不是好的idea。導緻Java程式更容易OOM (超過-XX :MaxPermSize.上限)
在Hospot中
而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地
記憶體中實作的元空間(Metaspace)來代替。永久帶是在JAVA虛拟機記憶體。
- 元空間的本質和永久代類似,都是對JVM規範中方法區的實作。不過元空間與永代最大的差別在于**:元空間不在虛拟機設定的記憶體中,而是使用本地記憶體。**
- 永久代、元空間二者并不隻是名字變了,内部結構也調整了。
- 根據《Java虛拟機規範》的規定,如果方法區無法滿足新的記憶體配置設定需求時,将抛出OOM異常。
四、設定方法區大小的參數
方法區的大小不必是固定的,jvm可 以根據應用的需要動态調整。
jdk7及以前:
➢通過-xx: PermSize來設定永久代初始配置設定空間。預設值是20. 75M
➢-XX:MaxPe rmSi ze來設定永久代最大可配置設定空間。32位機器預設是64M, 64位機器模式是82M
➢當JVM加載的類信 息容量超過了這個值,會報異常OutOfMemoryError : PermGenspace。
不過我使用的是jdk8,我們跑起來看一下:
證明在8裡面已經不可以使用了。
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設定為一個相對較高的值。
21807104/1024/1024=20.796875m
104857600/1024/1024=100
五、OOM:PermGen和OOM:Metaspace
如何解決這些OOM?
1、要解決OOM異常或heap space的異常,一般的手段是首先通過記憶體映像分析工具.(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是确認記憶體中的對象是否是必要的,也就是要先厘清楚到底是出現了記憶體洩漏(MemoryLeak)還是記憶體溢出(Memory Overflow )
2、如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到GC Roots的引用鍊。于是就能找到洩漏對象是通過怎樣的路徑與GCRoots相關聯并導緻垃圾收集器無法自動回收它們的。掌握了洩漏對象的類型資訊,以及GC Roots引用鍊的資訊,就可以比較準确地定位出洩漏代碼的位置。
3、如果不存在記憶體洩漏,換句話說就是記憶體中的對象确實都還必須存活着,那就應當檢查虛拟機的堆參數(-Xmx與-Xms) ,與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期的記憶體消耗。
六、方法區内部結構
第一部分:
《深入了解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.class])
●方法參數的數量和類型(按順序)
方法的修飾符(public, private,, protected, static, final,。synchronized, native, abstract的一個子集)
●方法的位元組碼(bytecodes)、操作數棧、局部變量表及大小(abstract和native方法除外)
●異常表 (abstract和native方法除外)
➢每個異常處理的開始位置、結束位置、代碼處理在程式計數器中的偏移位址、
被捕獲的異常類的常量池索引
來舉例子
準備:
我們通過控制台指令javap 進行反編譯,注意javap -v -p,此處加上p是為了讓private修飾的這些也能夠出來。并加上 >test.txt檔案中,友善我們檢視
就會在目前路徑下産生這個“test.txt”檔案
加載到方法區中的類裡面記錄了自己是被哪個加載器加載的,加載器也會記錄自己加載了哪些類
他們互相記錄
注意,這是我們通過位元組碼檔案看的,不是記憶體。最終它會被類加載器加載到方法區中。
同樣,位元組碼這個順序也是一緻的。
現有num,再有str。
方法就比較豐富一些
在源代碼中我們是沒有聲明構造器的,它會預設給我們提供一個無參構造器
在位元組碼檔案中它其實也是方法,叫做init方法——
隻是在源代碼層面構造器是構造器,方法是方法
我們來以test1()為例
比如我們再來看看這個靜态的吧
這個就是大家能夠看到的在加載器加載之前的位元組碼檔案資訊
我們還提到有的方法可能會有異常,那麼它的位元組碼中就會存在有 異常表
異常表在前面章節也講過
from,to:從哪到哪。2跟9是我們位元組碼檔案中的行号
23-27是我們代碼中的行号
即這個能夠包裹住的範圍。如果沒有出現異常, 這裡有一個 target
這個target 到我們的位元組碼12,位元組碼12代表goto到17行,在看一下LineNumberTable,就對應到我們的源代碼的28行,進源代碼看一下
其實就是直接到了return,就出去了。
第二部分:
下面再來說明一個問題:
non-final的類變量
●靜态變量和類關聯在一起,随着類的加載而加載,它們成為類資料在邏輯上的一部分。
●類變量被類的所有執行個體共享,即使沒有類執行個體時你也可以通路它。
來例子示範:
當然沒有問題,沒有NPE
補充說明:全局常量: static final。
被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就
會被配置設定了。
我們來反編譯一下Order這個類
在準備環節,對靜态屬性(static)進行初始化,再在initialization的時候指派1。若為final則直接在準備階段或者說編譯的時候就進行指派
七、class檔案中常量池的了解
運作時常量池 VS 常量池
位元組碼檔案中叫做常量池
- 方法區,内部包含了運作時常量池。
- 位元組碼檔案,内部包含了常量池。
- 要弄清楚方法區,需要了解清楚ClassFile,因為加載類的資訊都在方法區。
-
要弄清楚方法區的運作時常量池,需要了解清楚ClassFile中的常量池。.
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html。
如下:
可以看見,在我們位元組碼檔案中,是資訊量最大的一個
位元組碼檔案中的常量池加載到記憶體中的方法區以後,對應的結構就叫做運作時常量池。是以我們先弄清楚 常量池
為什麼需要提供一個常量池呢?
一個java源檔案中的類、接口,編譯後産生一個位元組碼檔案。而Java中的字 節碼需要資料支援,通常這種資料會很大以至于不能直接存到位元組碼裡,換另-種方式可以存到常量池,這個位元組碼包含了指向常量池的引用。在動态連結的時候會用到運作時常量池,之前有介紹。
比如:如下的代碼:
舉個例子:
就這麼去調,我們要使用的各種指令都是對常量池裡面結構的一些調用。
就好像是我們寫的代碼是菜,常量池中的内容是材料。具體執行的細節,都是從常量池裡面去調
常量池裡面有什麼呢?
小結:
常量池,可以看做是一張表,虛拟機指令根據這張常量表找到要執行的類
名、方法名、參數類型、字面量等類型。
八、運作時常量池的了解
- 運作時常量池( Runtime Constant Poo1)是方法區的一部分。
- 常量池表( Constant Pool Table)是Class檔案的一部分,用于存放編譯期生成的各種字面量與符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。
- 運作時常量池,在加載類和接口到虛拟機後,就會建立對應的運作時常量池。
-
JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的資料項像數組項一樣,是通過索引通路的。.運作時常量池中包含多種不同的常量,包括編譯期就已經明确的數值字面量,也包括到運作期解析後才能夠獲得的方法或者字段引用。此時不再是常量池中的符号位址了,這裡換為真實位址。
➢運作時常量池,相對于class檔案常量池的另一重要特征是:具備動态性。
- String.intern()如果string在常量池沒有就又放進去。
- 運作時常量池類似于傳統程式設計語言中的符号表(symbol table) ,但是它所包含的數,據卻比符号表要更加豐富一些。
- 當建立類或接口的運作時常量池時,如果構造運作時常量池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會抛OutOfMemoryError異常。
九、圖示舉例方法區的使用
通過javap -v -p MethodAreaDemo.class > text2.txt 反編譯
我們來看這個main方法
stack:操作數棧深度是3,局部變量表長度是5,參數大小是1
下面就是代碼的執行流程
LineNumberTabel左邊是源代碼的行号,右邊是位元組碼的号碼
LocalVariableTable:局部變量表
下面通過圖檔的方式展示一下執行過程:
之前有講如果不是static的話,0号是this,既然這裡是static方法,是以這裡的0号元素為它的參數args
我們發現在編譯階段其實操作數棧的深度和局部變量表的長度就已經确定下來了,其實局部變量表也就是數組,已經确定了長度的。
16.
十、方法區在jdk6、jdk7、jdk8中的演進細節
1.首先明确:隻有HotSpot才有永久代。
BEA JRockit、 IBM J9等來說,是不存在永久代的概念的。原則上如何實作。方法區屬于虛拟機實作細節,不受《Java虛拟機規範》管束,并不要求統一。
2.Hotspot中 方法區的變化:
jdk1.6及之前 | 有永久代(permanent generation),靜态比昂亮存放在永久代上 |
---|---|
jdk1.7 | 有永久代,但已經逐漸“去永久代”,字元串常量池、靜态變量移除,儲存在堆中 |
jdk1.8及以後 | 無永久代,類型資訊、字段、方法、常量儲存在本地記憶體的元空間,但字元串常量池、靜态變量仍在堆 |
JDK6
JDK7
虛拟記憶體
JDK8
永久代為什麼要被元空間替換?
http://openjdk.java.net/jeps/122
●随着Java8 的到來,HotSpot VM中再也 見不到永久代了。但是這 并不意味着類
的中繼資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個
區域叫做元空間( Metaspace )。
●由于類的中繼資料配置設定在本地記憶體中,元空間的最大可配置設定空間就是系統可用記憶體空間。
這項改動是很有必要的,原因有:
1)為永久代設定空間大小是很難确定的。
在某些場景下,如果動态加載類過多,容易産生Perm區的00M。比如某個實際Web.工程中,因為功能點比較多,在運作過程中,要不斷動态加載很多類,經常出現緻命錯誤。
"Exception in thread‘ dubbo client x.x connector’ java.langOutOfMemoryError: PermGenspace"
而元空間和永久代之間最大的差別在于:元空間并不在虛拟機中,而是使用本地記憶體。是以,預設情況下,元空間的大小僅受本地記憶體限制。
2)對永久代進行調優是很困難的。 【full gc很花時間】
十一、StringTable為什麼要調整位置
jdk7中将StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc
的時候才會觸發。而full gc是老年代的空間不足、永久代不足時才會觸發。
這就導緻StringTable回收效率不高。而我們開發中會有大量的字元串被建立,回收效率低,導緻永久代記憶體不足。放到堆裡,能及時回收記憶體。
十二、如何證明靜态變量存在哪
舉個例子,來看一下這個位元組數組會存在哪?
來看一下jdk6/7中
就放到這個老年代了。
來看一下jdk8中
這一節不是很清楚,建議自己閱讀資料
十三、方法區的垃圾回收行為
有些人認為方法區(如HotSpot虛拟機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛拟機規範》對方法區的限制是非常寬松的,提到過可以不要求虛拟機在方法區中實作垃圾收集。事實上也确實有未實作或未能完整實作方法區類型解除安裝的收集器存在(如JDK 11時 期的ZGC收集器就不支援類解除安裝)。
一般來說**這個區域的回收效果比較難令人滿意,尤其是類型的解除安裝,條件相當苛刻。但是這部分區域的回收有時又确實是必要的。**以前Sun公司的Bug清單中,曾出現過的若幹個嚴重的Bug就是由于低版本的HotSpot虛拟機對此區域未完全回收而導緻記憶體洩漏。
方法區的垃圾收集主要回收兩部分内容:常量池中廢棄的常量和不再使用的類型。
●先來說說方法區内常量池之中主要存放的兩大類常量:字面量和符号引用。
字面量比較接近Java語言層次的常量概念,如文本字元串、被聲明為final
的常量值等。而符号引用則屬于編譯原理方面的概念,包括下面三類常量:
➢1、類和接口的全限定名
➢2、字段的名稱和描述符
➢3、方法的名稱和描述符
●HotSpot虛拟機對常量池的回收政策是很明确的,隻要常量池中的常量沒有
被任何地方引用,就可以被回收。
●回收廢棄常量與回收Java堆中的對象非常類似。
●判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用
的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
➢該類所有的執行個體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的
執行個體。
➢加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載
器的場景,如OSGi、 JSP的重加載等,否則通常是很難達成的。
➢該類對應的java. 1ang . Class對象沒有在任何地方被引用,無法在任何地方通過
反射通路該類的方法。
●Java虛拟機被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允
許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛拟機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading檢視類加載和解除安裝資訊
●在大量使用反射、動态代理、CGLib等位元組碼架構,動态生成JSP以及oSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛拟機具備類型解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。
十四、運作時資料區的總結與常見大腸面試題說明
總結
百度
三面:說一下JVM記憶體模型吧,有哪些區?分别幹什麼的?
螞蟻金服:
Java8的記憶體分代改進
JVM記憶體分哪幾個區,每個區的作用是什麼?
一面: JVM記憶體分布/記憶體結構?棧和堆的差別?堆的結構?為什麼兩個survivor區?
二面: Eden和Survior的比例配置設定
小米:
jvm記憶體分區,為什麼要有新生代和老年代
位元組跳動:
二面: Java的記憶體分區
二面:講講jvm運作時資料庫區
什麼時候對象會進入老年代?
京東:
JVM的記憶體結構,Eden和Survivor比例 。
JVM記憶體為什麼要分成新生代,老年代,持久代。新生代中為什麼要分為Eden和Survivor。
天貓:
一面: Jvm記憶體模型以及分區,需要詳細到每個區放什麼。
一面: JVM的記憶體模型,Java8做了什麼修改
拼多多:
JVM記憶體分哪幾個區,每個區的作用是什麼?
美團:
java記憶體配置設定
jvm的永久代中會發生垃圾回收嗎?.
一面: jvm記憶體分區,為什麼要有新生代和老年代?