09-方法区
方法區
前言
這次所講述的是運行時數據區的最后一個部分
從線程共享與否的角度來看
ThreadLocal:如何保證多個線程在并發環境下的安全性?典型應用就是數據庫連接管理,以及會話管理
棧、堆、方法區的交互關系
下面就涉及了對象的訪問定位
- Person:存放在元空間,也可以說方法區
- person:存放在Java棧的局部變量表中
- new Person():存放在Java堆中
方法區的理解
《Java虛擬機規范》中明確說明:“盡管所有的方法區在邏輯上是屬于堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。”但對于HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
所以,方法區看作是一塊獨立于Java堆的內存空間。
方法區主要存放的是 Class,而堆中主要存放的是 實例化的對象
- 方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域。
- 方法區在JVM啟動的時候被創建,并且它的實際的物理內存空間中和Java堆區一樣都可以是不連續的。
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
- 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤:java.lang.OutofMemoryError:PermGen space (jdk7及以前)或者java.lang.OutOfMemoryError:Metaspace(jdk8及以后)
- 加載大量的第三方的jar包
- Tomcat部署的工程過多(30~50個)
- 大量動態的生成反射類
- 關閉JVM就會釋放這個區域的內存。
HotSpot中方法區的演進
在jdk7及以前,習慣上把方法區,稱為永久代。jdk8開始,使用元空間取代了永久代。
- JDK 1.8后,元空間存放在堆外內存中
本質上,方法區和永久代并不等價。僅是對hotspot而言的。《Java虛擬機規范》對如何實現方法區,不做統一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
現在來看,當年使用永久代,不是好的idea。導致Java程序更容易oom(超過-XX:MaxPermsize上限)
而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Metaspace)來代替
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代最大的區別在于:元空間不在虛擬機設置的內存中,而是使用本地內存
永久代、元空間二者并不只是名字變了,內部結構也調整了
根據《Java虛擬機規范》的規定,如果方法區無法滿足新的內存分配需求時,將拋出OOM異常(java.lang.OutOfMemoryError:Metaspace)
設置方法區大小與OOM
方法區的大小不必是固定的,JVM可以根據應用的需要動態調整。
jdk7及以前
- 通過-XX:Permsize來設置永久代初始分配空間。默認值是20.75M
- -XX:MaxPermsize來設定永久代最大可分配空間。32位機器默認是64M,64位機器模式是82M
- 當JVM加載的類信息容量超過了這個值,會報異常OutofMemoryError:PermGen space。
-XX:Permsize=100m -XX:MaxPermsize=100m
JDK8以后
元數據區大小可以使用參數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
默認值依賴于平臺。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制,為本地內存空間的最大值。
與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。如果元數據區發生溢出,虛擬機一樣會拋出異常OutOfMemoryError:Metaspace
-XX:MetaspaceSize:設置初始的元空間大小。對于一個64位的服務器端JVM來說,其默認的-XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,FullGC將會被觸發并卸載沒用的類(即這些類對應的類加載器不再存活)然后這個高水位線將會重置。新的高水位線的值取決于GC后釋放了多少元空間。如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值。如果釋放空間過多,則適當降低該值。
如果初始化的高水位線設置過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日志可以觀察到FullGC多次調用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設置為一個相對較高的值。
如何解決這些OOM
-
要解決OOM異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer/IDEA Visual VM)對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)
- 內存泄漏:有大量的引用指向某些對象,但是這些對象以后不會使用了,但是因為它們還和GC ROOT有關聯,所以導致以后這些對象也不會被回收,這就是內存泄漏的問題,內存泄漏最終也會造成OOM
-
如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GCRoots相關聯并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GCRoots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
-
如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(-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)
- 方法參數的數量和類型(按順序)
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
- 方法的字節碼(bytecodes)、操作數棧的深度、局部變量表及參數的大小(abstract和native方法除外)(對非靜態方法,第一個參數是this,靜態方法沒有this對象)
- 異常表(abstract和native方法除外)
每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引
方法區存儲的類信息記錄字節碼文件中的信息,此外,還保留了加載該類使用的類加載器,二者獨立加載,類加載器也記錄了它加載了誰。
查看字節碼的兩種方法:
- 反編譯:命令行輸入:javap -v -p 類名.class > test.txt
(“-p”只為了將代碼中private修飾的代碼反編譯出來,若不存在private修飾的代碼,則可不加)("> test.txt":并將反編譯結果寫入test.txt文件,非必需) - 插件:jclasslib
non-final的類變量
靜態變量和類關聯在一起,隨著類的加載而加載,他們成為類數據在邏輯上的一部分
類變量被類的所有實例共享,即使沒有類實例時,你也可以訪問它
/*** non-final的類變量*/ public class MethodAreaTest {public static void main(String[] args) {Order order = new Order();order.hello();System.out.println(order.count);} } class Order {public static int count = 1;public static final int number = 2;public static void hello() {System.out.println("hello!");} }如上代碼所示,即使我們把order設置為null,也不會出現空指針異常
補充說明:全局常量(final static)
全局常量就是使用 static final 進行修飾
被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
運行時常量池 VS 常量池
運行時常量池,就是運行時常量池
- 方法區,內部包含了運行時常量池
- 字節碼文件,內部包含了常量池
- 要弄清楚方法區,需要理解清楚C1assFile,因為加載類的信息都在方法區。
- 要弄清楚方法區的運行時常量池,需要理解清楚classFile中的常量池。
常量池
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外,還包含一項信息就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符號引用
為什么需要常量池
一個java源文件中的類、接口,編譯后產生一個字節碼文件。而Java中的字節碼需要數據支持,通常這種數據會很大以至于不能直接存到字節碼里,換另一種方式,可以存到常量池,這個字節碼包含了指向常量池的引用。在動態鏈接的時候會用到運行時常量池,之前有介紹。
比如:如下的代碼:
public class SimpleClass {public void sayHello() {System.out.println("hello");} }雖然上述代碼只有194字節,但是里面卻使用了String、System、PrintStream及Object等結構。這里的代碼量其實很少了,如果代碼多的話,引用的結構將會更多,這里就需要用到常量池了。
常量池中有什么
- 數量值
- 字符串值
- 類引用
- 字段引用
- 方法引用
例如下面這段代碼
public class MethodAreaTest2 {public static void main(String args[]) {Object obj = new Object();} }將會被翻譯成如下字節碼
new #2 dup invokespecial小結
常量池,可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
運行時常量池,在加載類和接口到虛擬機后,就會創建對應的運行時常量池。
JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的。
運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
運行時常量池,相對于Class文件常量池的另一重要特征是:具備動態性。
- 如String.intern():如果常量池中沒有String,那么就得將其添加到運行時常量池中,即運行時常量池中的內容比常量池更多。
運行時常量池類似于傳統編程語言中的符號表(symboltable),但是它所包含的數據卻比符號表要更加豐富一些。
當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋outofMemoryError異常。
方法區使用舉例
如下代碼
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);} }字節碼執行過程展示
首先現將操作數500放入到操作數棧中
然后存儲到局部變量表中
然后重復一次,把100放入局部變量表中,最后再將變量表中的500 和 100 取出,進行操作
將500 和 100 進行一個除法運算,在把結果入棧
在最后就是輸出流,需要調用運行時常量池的常量
最后調用invokevirtual(虛方法調用),然后返回
返回時
程序計數器始終計算的都是當前代碼運行的位置,目的是為了方便記錄 方法調用后能夠正常返回,或者是進行了CPU切換后,也能回來到原來的代碼進行執行。
方法區的演進細節-hotspot
首先明確:只有Hotspot才有永久代。BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實現方法區屬于虛擬機實現細節,不受《Java虛擬機規范》管束,并不要求統一
Hotspot中方法區的變化:
| JDK1.7 | 有永久代,但已經逐步 “去永久代”,字符串常量池,靜態變量移除,保存在堆中 |
| JDK1.8 | 無永久代,類型信息,字段,方法,常量保存在本地內存的元空間,但字符串常量池、靜態變量仍然在堆中。 |
JDK6的時候
JDK7的時候
JDK8的時候,元空間大小只受物理內存影響
為什么永久代要被元空間替代?
是JRockit和HotSpot融合后的結果,因為JRockit沒有永久代,所以他們不需要配置永久代
隨著Java8的到來,HotSpot VM中再也見不到永久代了。但是這并不意味著類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域叫做元空間(Metaspace)。
由于類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間,這項改動是很有必要的,原因有:
- 為永久代設置空間大小是很難確定的。
在某些場景下,如果動態加載類過多,容易產生Perm區的oom。比如某個實際Web工
程中,因為功能點比較多,在運行過程中,要不斷動態加載很多類,經常出現致命錯誤。
設置過小,則會頻繁Full GC,進而導致頻繁STW,拖慢程序性能,且若Full GC后類未被及時回收,仍需使用,則可能出現OOM,導致程序終止;但是設置過大,又會浪費資源。
“Exception in thread‘dubbo client x.x connector’java.lang.OutOfMemoryError:PermGen space”
而元空間和永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。
因此,默認情況下,元空間的大小僅受本地內存限制。這樣的話,就不易出現OOM,同時,也不會頻繁GC。
- 對永久代進行調優是很困難的。
- 主要是為了降低Full GC。雖然方法區的垃圾回收次數比較少,但仍存在Full GC,方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的變量和不再使用的類型。但判斷類或者常量不再被使用且需要被回收是比較耗時的,即Full GC是比較耗時的,因此,我們盡量要減少Full GC,這樣,使用元空間就是很好的選擇。
有些人認為方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄漏
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不在使用的類型
StringTable(字符串常量池)為什么要調整位置
jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才會觸發。而fullgc是老年代的空間不足、永久代不足時才會觸發。
這就導致StringTable回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。放到堆里,能及時回收內存。
靜態變量存放在那里?
靜態引用對應的對象實體始終都存在堆空間,即new的對象都是存在堆空間中的。
可以使用 jhsdb.exe,需要在jdk9的時候才引入的,監控進程細節
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虛擬機對此區域未完全回收而導致內存泄漏。
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。
回收廢棄常量與回收Java堆中的對象非常類似。(關于常量的回收比較簡單,重點是類的回收)
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如osGi、JSP的重加載等,否則通常是很難達成的。 - 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。I Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class 以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息
- 在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。
總結
常見面試題
百度
三面:說一下JVM內存模型吧,有哪些區?分別干什么的?
螞蟻金服:
Java8的內存分代改進
JVM內存分哪幾個區,每個區的作用是什么?
一面:JVM內存分布/內存結構?棧和堆的區別?堆的結構?為什么兩個survivor區?
二面:Eden和survior的比例分配
小米:
jvm內存分區,為什么要有新生代和老年代
字節跳動:
二面:Java的內存分區
二面:講講vm運行時數據庫區
什么時候對象會進入老年代?
京東:
JVM的內存結構,Eden和Survivor比例。
JVM內存為什么要分成新生代,老年代,持久代。新生代中為什么要分為Eden和survivor。
天貓:
一面:Jvm內存模型以及分區,需要詳細到每個區放什么。
一面:JVM的內存模型,Java8做了什么改
拼多多:
JVM內存分哪幾個區,每個區的作用是什么?
美團:
java內存分配
jvm的永久代中會發生垃圾回收嗎?
一面:jvm內存分區,為什么要有新生代和老年代?
總結
- 上一篇: 08-堆
- 下一篇: 06- 本地方法接口