探索 JVM 的隱秘角落:元空間詳解
隨著Java應(yīng)用程序日益復(fù)雜和龐大,JVM(Java虛擬機(jī))的性能優(yōu)化變得尤為重要。在JVM的各種組件中,元空間(Metaspace)作為類元數(shù)據(jù)的存儲(chǔ)區(qū)域,扮演著關(guān)鍵角色。本文將深入探討JVM元空間的工作原理、架構(gòu)設(shè)計(jì)及其對(duì)應(yīng)用性能的影響,并提供實(shí)際的調(diào)優(yōu)建議。通過本文,讀者不僅能夠全面了解元空間的基本概念,還能掌握如何有效管理和優(yōu)化這一重要資源。
什么是JVM方法區(qū)
方法區(qū)主要是用于存儲(chǔ)類信息、靜態(tài)變量以及常量信息的。是各個(gè)線程共享的一個(gè)區(qū)域。我們都知道JVM中有個(gè)區(qū)域叫堆區(qū),所以有時(shí)候人們也會(huì)稱方法區(qū)為Non-Heap(非堆)。
在JDK8之前方法區(qū)存放在一個(gè)叫永久代的空間里。 在JDK8之后由于HotSpot 和JRockit 的合并,所以方法區(qū)就被作為元數(shù)據(jù)區(qū)了。
方法區(qū)和永久代是什么關(guān)系?
其實(shí)方法區(qū)并不是一個(gè)實(shí)際的區(qū)域,他不過是JVM虛擬機(jī)規(guī)范提出的一個(gè)概念而已。在HotSpot 實(shí)現(xiàn)方法區(qū)的方式就在JVM內(nèi)存中劃分一個(gè)區(qū)域作為永久代來存放這些數(shù)據(jù)。
在JDK8之前我們可以用下面的參數(shù)來調(diào)整永久代的大小
-XX:PermSize=N //方法區(qū) (永久代) 初始大小
-XX:MaxPermSize=N //方法區(qū) (永久代) 最大大小,超過這個(gè)值將會(huì)拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen
為什么JDK8之后要把永久代 (PermGen)換成元數(shù)據(jù)區(qū)(MetaSpace)
將數(shù)據(jù)放在永久代固然沒問題,但是隨著時(shí)間的推移,方法區(qū)使用的空間可能會(huì)逐漸變大,若我們分配大小不當(dāng)很可能造成線上OOM問題,所以設(shè)計(jì)者們就在方法區(qū)移動(dòng)到本地內(nèi)存中,通過本地內(nèi)存來存放數(shù)據(jù)。并且元數(shù)據(jù)區(qū)默認(rèn)分配值為unlimited(我們也可以通過-XX:MetaspaceSize來動(dòng)態(tài)調(diào)整),理論上是沒有明確大小,是可以動(dòng)態(tài)分配空間的,這樣一來由于元數(shù)據(jù)區(qū)就不會(huì)受到JVM內(nèi)存分配的約束了,所以理論上發(fā)生OOM的概率會(huì)小于永久代。
深入理解Java虛擬機(jī)關(guān)于方法區(qū)的說法
筆者查閱權(quán)威《深入理解Java虛擬機(jī)》 中看到,《Java虛擬機(jī)規(guī)范》 對(duì)于方法區(qū)的實(shí)現(xiàn)即元空間或者永久代垃圾回收行為沒有強(qiáng)制要求。 原因很簡(jiǎn)單,方法區(qū)進(jìn)行垃圾收集的回收的收益不是很大,它并不像堆內(nèi)存的新生代那樣,在一次新生代的垃圾回收就能回收70%-90% 的內(nèi)存空間。這也使得大部分人(包括筆者)認(rèn)為方法區(qū)不涉及GC的,實(shí)際上對(duì)于jdk8 版本的Hotspot虛擬機(jī)而言,JVM 中某一個(gè)類符合以下這3個(gè)條件時(shí)將會(huì)卸載類并回收這個(gè)類的元數(shù)據(jù)空間:
- 在堆中沒有任何基于當(dāng)前類或者基于該類派生子類的實(shí)例。
- 該類的java.lang.Class對(duì)象沒有在任何地方被引用,以及無法通過反射等方式訪問該類的方法。
- 加載該類的類加載器被回收,這個(gè)條件除非是精心設(shè)計(jì)過的可替換類加載器的場(chǎng)景,否者很難實(shí)現(xiàn)。
需要注意的是,在判斷是否有實(shí)例還在使用當(dāng)前類以及是否有類加載器引用這個(gè)類這兩個(gè)步驟的時(shí)候,為了能夠明確這兩點(diǎn),可能需要掃描全部堆空間的,這也就意味著元空間的回收可能伴隨著FullGC。
代理對(duì)象創(chuàng)建不當(dāng)導(dǎo)致元空間OOM問題
可以看到最后一點(diǎn)比較苛刻,所以就導(dǎo)致如果我們使用Spring等框架通過增強(qiáng)技術(shù)生成大量的新類型載入元空間內(nèi)存,導(dǎo)致元空間內(nèi)存溢出(Caused by: java.lang.OutOfMemoryError: Metaspace) ,就像下面這段代碼一樣,為了更快看到效果,我們手動(dòng)設(shè)置一下元空間大小-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m:
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
//設(shè)置代理目標(biāo)
enhancer.setSuperclass(EmptyObject.class);
enhancer.setUseCache(false);
//設(shè)置單一回調(diào)對(duì)象,在調(diào)用中攔截對(duì)目標(biāo)方法的調(diào)用
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(objects, args));
enhancer.create();
}
}
我們通過jconsole定位查看當(dāng)前進(jìn)程的類加載信息:
可以看到大量EmptyObject的增強(qiáng)類被加載至元空間中:
鍵入命令jmap 定位加載的類信息再次進(jìn)行確認(rèn):
jmap -histo 4532
可以看到生成了大量的net.sf.cglib.proxy相關(guān)的類
num #instances #bytes class name
----------------------------------------------
1: 3824742 600680704 [C
2: 1932145 170028760 java.lang.reflect.Method
3: 3806008 91344192 java.lang.String
4: 1779516 37754664 [Ljava.lang.Class;
5: 26568 15064520 [I
6: 618402 14841648 net.sf.cglib.core.Signature
7: 79344 12595728 java.lang.Class
8: 154765 12381200 java.lang.reflect.Constructor
9: 308844 9883008 net.sf.cglib.proxy.MethodProxy
10: 308844 9883008 net.sf.cglib.proxy.MethodProxy$CreateInfo
我們以MethodProxy進(jìn)行定位可以看到這個(gè)類是在create方法創(chuàng)建的,這也就意味著上述代碼的最后一個(gè)create方法會(huì)創(chuàng)建大量的MethodProxy并存到元空間中導(dǎo)致元空間內(nèi)存溢出:
public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
proxy.sig1 = new Signature(name1, desc);
proxy.sig2 = new Signature(name2, desc);
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}
所以盡管說jdk8將類信息存到原空間中,但我們?nèi)粘_M(jìn)行開發(fā)也需要留意對(duì)于cglib等增強(qiáng)技術(shù)的使用是否得當(dāng),如果發(fā)現(xiàn)大量的增強(qiáng)類出現(xiàn)在元空間時(shí),需要及時(shí)定位并解決。