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