Metaspace 是 Java 8 起存放類元數據的地方,走的是本地記憶體而非堆。報
OutOfMemoryError: Metaspace,多半不是物件太多,而是動態生成的類太多,或者某個 ClassLoader 洩漏導致舊類卸載不掉。先查是不是-XX:MaxMetaspaceSize設太小,再查類加載器洩漏。
Issue & Env (案發現場與環境)
Java 8 之前,類的元數據放在永久代(PermGen),大小由 -XX:MaxPermSize 定死。PermGen 大小難預估,設小了報 OutOfMemoryError: PermGen space,設大了浪費堆空間。
Java 8 引入 Metaspace 替換 PermGen。類的名稱、字段、方法、字節碼、常量池、註解等信息,現在都落到本地記憶體的一個區域裡。預設它沒有硬上限,會自動擴展,直到操作系統沒記憶體可給。
受影響版本區間和是否強設上限有關:
- 沒設
-XX:MaxMetaspaceSize時,Metaspace 預設持續擴張,直到耗盡系統記憶體。 - 顯式設了小值(比如
128m),動態類一多就撞頂。
容易觸發的場景:大量用 CGLIB 或 ASM 動態生成類、反射和動態代理氾濫、web 應用反覆熱部署但舊 ClassLoader 沒釋放。
StackTrace(堆疊日誌)
應用跑著跑著崩掉,報錯落在類定義這一環:
java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:1096)
at java.net.URLClassLoader.access$100(URLClassLoader.java:101)
at java.net.URLClassLoader$1.run(URLClassLoader.java:683)
at java.net.URLClassLoader$1.run(URLClassLoader.java:682)
at java.security.AccessController.doPrivileged(Native Method)
線上案例裡,catalina.out 也這麼出現過:
2014-09-25 14:56:32,907 localhost-startStop-1 ERROR ...
java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at org.apache.catalina.loader.WebappClassLoader.findClassInternal(WebappClassLoader.java:2957)
注意堆疊落在 defineClass 上。這說明是「正在加載新類」時空間不夠,跟堆裡物件太多是兩碼事。
Reproduce(復現程式碼)
要穩定復現,給一個小上限,再用 CGLIB 循環生成類即可。這裡用 -XX:MaxMetaspaceSize=32m 把門卡死:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class MetaspaceLeak {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 每次生成一个新类,塞进 Metaspace
}
}
}
運行:
java -XX:MaxMetaspaceSize=32m -cp .:cglib.jar MetaspaceLeak
過不了多久就拋 OutOfMemoryError: Metaspace。
Root Cause(根因分析)
Metaspace 的用量取決於兩件事:加載了多少類、每個類的元數據有多大。所以報錯的主因要麼是類太多、要麼是類太大。
更深一層是「類卸載」失靈。Metaspace 裡的類元數據能不能回收,取決於把它加載進來的 ClassLoader 有沒有被回收。ClassLoader 之間是父子的引用關係,只要還有引用指向某個 loader,它加載的所有類元數據都別想卸載。
web 應用熱部署是重災區。每次重新部署,容器用一個新 ClassLoader 加載同樣的類,舊 loader 本該被丟棄。但只要有執行緒、靜態字段或框架緩存還拽著舊 loader 的一條引用,舊的類元數據就一直在 Metaspace 裡堆著。反覆部署幾次,元空間就滿了。
另一個主因是上限設太小。預設不設上限才是 Java 8 的設計意圖,手動把 MaxMetaspaceSize 壓得太低,一箇中型應用的正常類量都裝不下。
MetaspaceSize 這個參數容易誤解,它不是「初始大小」,而是觸發一次 full GC 嘗試卸載類的閾值。到不了這個值,JVM 不會主動去回收類元數據。
The Fix(解決方案)
報錯先別急著找 bug,確認是不是上限卡死了:
java -XX:MaxMetaspaceSize=512m -jar myapp.jar
或者乾脆去掉 -XX:MaxMetaspaceSize,交給 JVM 自動擴展。Atlassian 的官方排查文檔給的就是這個建議:讓 Java 8 按需自動增加 Metaspace。
上限放開了還漲,那就是有 ClassLoader 一直沒被回收。看 Metaspace 用量用 jstat:
jstat -gcmetacapacity <pid>
關注 MC(當前元數據容量)和 MU(已用)是不是一路上漲不回落。再用 jcmd <pid> GC.class_stats 看每個類加載器名下佔了多少類、多少字節。定位到洩漏的 loader 後,切斷引用鏈:清空靜態字段舊引用、停掉沒退出的執行緒、別讓框架緩存一直抓著舊 ClassLoader。
動態生成類要收斂。CGLIB、ASM 預設會緩存生成的類,別為了省事關掉緩存。enhancer.setUseCache(true) 能讓同類名代理複用,不重複塞元數據。反射和動態代理也一樣,能複用就複用。
作為緩解,把 MetaspaceSize 調低能讓觸發類卸載的 GC 來得更早:
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -jar myapp.jar
這只是緩解,不治本。真洩漏還得回源頭掐斷引用鏈。
