1、 棧、堆、方法區的交互關係
2、 方法區的理解
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
方法區在哪裏?
《Java虛擬機規範》中明確說明: “儘管所有的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。” 但對於HotSpot JVM而言,方法區還有一個別名叫做Non-Heap (非堆), 目的就是要和堆分開。
所以,方法區看作是一塊獨立於Java堆的內存空間。
- 方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域。
- 方法區在JVM啓動的時候被創建,並且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的。
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
- 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤: java.lang.OutOfMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace
- 加載大量的第三方的jar包;Tomcat 部署的工程過多(30-50個);大量動態的生成反射類
- 關閉JVM就會釋放這個區域的內存
Hotspot中方法區的演進
- 在jdk7及以前,習慣上把方法區,稱爲永久代。jdk8開始,使用元空間取代了永久代。
- 本質上,方法區和永久代並不等價。僅是對hotspot而言的。《Java虛擬機規範》對如何實現方法區,不做統一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。
> 現在來看,當年使用永久代,不是好的idea。導致Java程序更容易00M (超過 -XX: MaxPermsize上限)
- 而到了JDK 8,終於完全廢棄了永久代的概念,改用與JRockit, J9一樣在本地內存中實現的元空間(Metaspace)來代替
- 元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機設置的內存中,而是使用本地內存。
- 永久代、元空間二者並不只是名字變了,內部結構也調整了。
- 根據《Java虛擬機規範》的規定,如果方法區無法滿足新的內存分配需求時,將拋出O0M異常。
3、 設置方法區大小與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設置爲一個相對較高的值。
如何解決這些OOM?
1、要解決OOM異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)
2、如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。於是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
3、如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx與-Xms) ,與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序雲行期的內存消耗。
4、 方法區的內部結構
方法區( 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方法除外)
> 每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、
被捕獲的異常類的常量池索引
non-final的類變量
- 靜態變量和類關聯在一起,隨着類的加載而加載,它們成爲類數據在邏輯上的一部分。
- 類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。
補充說明:全局常量 static final
被聲明爲 final 的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
- 方法區,內部包含了運行時常量池。
- 字節碼文件,內部包含了常量池。
- 要弄清楚方法區,需要理解清楚ClassFile,因爲加載類的信息都在方法區
- 要弄清楚方法區的運行時常量池,需要理解清楚ClassFile中的常量池。
如下:
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Pool Table) ,包括各種字面量和對類型、域和方法的符號引用。
爲什麼需要常量池?
一個java源文件中的類、接口,編譯後產生一個字節碼文件。而Java中的字節碼需要數據支持,通常這種數據會很大以至於不能直接存到字節碼裏,換另一種方式,可以存到常量池,這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池,之前有介紹。
比如: 如下的代碼:
雖然只有194字節,但是裏面卻使用了String、 System、 PrintStream及
Object等結構。這裏代碼量其實已經很小了。如果代碼多,引用到的結構會更多!這裏就需要常量池了!
常量池中有什麼?
幾種在常量池內存儲的數據類型包括:
- 數量值
- 字符串值
- 類引用
- 字段引用
- 方法引用
例如下面這段代碼:
Obiect foo = new Object () ;
將會被編譯成如下字節碼:
小結:
常量池,可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
運行時常量池
- 運行時常量池(Runtime Constant Pool)是方法區的一部分。
- 常量池表(Constant Pool Table)是Class文件的一部分,用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。
- 運行時常量池,在加載類和接口到虛擬機後,就會創建對應的運行時常量池。
- JVM爲每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的。
- 運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這裏換爲真實地址
> 運行時常量池,相對於Class文件常量池的另一重要特徵是:具備動態性。
- 運行時常量池類似於傳統編程語言中的符號表(symbol table) ,但是它所包含的數據卻比符號表要更加豐富一些。
- 當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。
5、 方法區使用舉例
6、 方法區的演進細節
1、首先明確:只有HotSpot纔有永久代。
BEA JRockit. IBM J9等來說,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,不受《Java虛擬機規範》管束,並不要求統一。
2、 Hotspot中方法區的變化:
jdk1.6及之前 |
有永久代(permanent generation) ,靜態變量存放在永久代上 |
jdk1.7 |
有永久代,但已經逐步“去永久代” ,字符串常量池、靜態變量移除,保存在堆中 |
jdk1.8及之後 |
無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆 |
永久代爲什麼要被元空間替換?
http://openjdk.java.net/jeps/122
- 隨着Java8 的到來, HotSpot VM中再也見不到永久代了。但是這並不意味着類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫做元空間(Metaspace)
- 由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。
- 這項改動是很有必要的,原因有
1)爲永久代設置空間大小是很難確定的。
在某些場景下,如果動態加載類過多,容易產生Perm區的OOM。比如某個實際Web工程中,因爲功能點比較多,在運行過程中,要不斷動態加載很多類,經常出現致命錯誤。
"Exception in thread 'dubbo client x.x connector' java.lang.OutOtMemoryError: PermGen
space"
而元空間和永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。
2)對永久代進行調優是很困難的。
StringTable爲什麼要調整?
jdk7中將StringTable放到了堆空間中。因爲永久代的回收效率很低,在full gc的時候纔會觸發。而full gc是老年代的空間不足、永久代不足時纔會觸發。
這就導致StringTable回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆裏,能及時回收內存。
7、 方法區的垃圾回收
有些人認爲方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行爲的,其實不然。《Java虛擬機規範》對方法區的約束是非常寬鬆的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK 11時期的ZGC收集器就不支持類卸載)
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前Sun公司的Buq列表中,曾出現過的若干個嚴重的Bug就是由於低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏。
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
- 先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
1、類和接口的全限定名
2、字段的名稱和描述符
3、方法的名稱和描述符
- HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。
- 回收廢棄常量與回收Java堆中的對象非常類似。
- 判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、 JSP的重加載等,否則通常是很難達成的。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法,
- java虛擬機被允許對滿足上述三個條件的無用類進行回收,這裏說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。關於是否要對類型進行回收HotSpot虛擬機提供了
-Xnoclassgc參數進行控制,還可以使用-verbose: class以及-XX:+Traceclass-Loading,
-XX:+TraceClassUnLoading查看類加載和卸載信息
- 在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。
8、 總結
常見面試題
百度
三面:說一下JVM內存模型吧,有哪些區?分別幹什麼的?
螞蟻金服:
Java8的內存分代改進
JVM內存分哪幾個區,每個區的作用是什麼?
一面: JVM內存分佈/內存結構?棧和堆的區別?堆的結構?爲什麼兩個survivor區?
二面: Eden和survior的比例分配
小米:
jvm內存分區,爲什麼要有新生代和老年代
字節跳動:
二面: Java的內存分區
二面:講講jvm運行時數據庫區什麼時候對象會進入老年代?
京東:
JVM的內存結構, Eden和Survivor比例。
JVM內存爲什麼要分成新生代,老年代,持久代。新生代中爲什麼要分爲Eden和Survivor.
天貓:
一面: JVM內存模型以及分區,需要詳細到每個區放什麼。
一面: JVM的內存模型, Java8做了什麼修改
拼多多:
JVM內存分哪幾個區,每個區的作用是什麼?
美團:
java內存分配
jvm的永久代中會發生垃圾回收嗎?
一面: jvm內存分區,爲什麼要有新生代和老年代?