StackTraces
Java已驗證

Java OutOfMemoryError: Metaspace 排查與修復

@debug_master32 天前更新閱讀 6 min00

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

這只是緩解,不治本。真洩漏還得回源頭掐斷引用鏈。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法