首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

JVM 內存增長問題排查實例:Groovy 動態腳本導致的類加載洩漏

     編輯:Java 綜合教學

JVM 內存增長問題排查實例:Groovy 動態腳本導致的類加載洩漏

記錄一次生產環境 JVM 內存持續增長的排查過程,核心原因是動態腳本編譯產生的類未被回收,導致 Metaspace / PermGen 區塊耗盡。

現象描述

運維發現應用發布後 JVM 內存佔用回落,隨後持續升高。對 Heap Dump 進行分析,發現大量 GroovyClassLoader 加載的類對象佔據內存。

問題代碼定位

public  T getScriptedObject(String scriptName, String scriptSource, Class cls) {
    if (StringUtils.isEmpty(scriptSource))
        throw new RuntimeException("服務腳本" + scriptName + "為空");

    GroovyObject goo = null;
    Class clz = null;
    try {
        // 每次調用都解析腳本源碼,生成新的 Class
        clz = groovyClassLoader.parseClass(scriptSource);
        goo = (GroovyObject) clz.newInstance();

        // 關鍵問題:自動裝配導致 Bean 成為多例
        if (null != beanFactory) {
            beanFactory.autowireBeanProperties(goo, 1, true); // AutowireCapableBeanFactory.AUTOWIRE_BY_NAME
        }
    } catch (UnsatisfiedDependencyException ex) {
        // 忽略異常
    } catch (Exception ex) {
        logger.error("腳本{}異常:{}", scriptName, ex);
        throw new RuntimeException(ex);
    }

    if (cls.isAssignableFrom(goo.getClass())) {
        return (T) goo;
    } else {
        throw new RuntimeException("腳本" + scriptName + "錯誤");
    }
}

根因分析

  1. 動態編譯產生新類:每次調用 parseClass 都會生成一個新的 Class 對象,由 GroovyClassLoader 加載。若腳本內容不變,仍重複編譯,導致類數量無限增長。
  2. Spring 自動裝配導致多例:beanFactory.autowireBeanProperties(goo, 1, true) 將動態生成的對象納入 Spring 容器管理,但每次調用都創建新實例,且未註冊為單例 Bean,實際上產生了大量「多例」對象,佔用 Heap 內存。
  3. 類卸載條件不滿足:GroovyClassLoader 及其加載的類長期被引用,無法被 GC 回收(除非類加載器本身不可達)。

解決方案

引入腳本緩存層:使用 Map 緩存已編譯的腳本類,避免重複編譯。

private final Map> scriptClassCache = new ConcurrentHashMap<>();

public  T getScriptedObject(String scriptName, String scriptSource, Class cls) {
    // 1. 從緩存獲取或編譯類
    Class clz = scriptClassCache.computeIfAbsent(scriptName, k -> {
        try {
            return groovyClassLoader.parseClass(scriptSource);
        } catch (Exception e) {
            throw new RuntimeException("腳本編譯失敗: " + scriptName, e);
        }
    });

    // 2. 實例化(每次調用仍可創建新實例,或根據需求緩存實例)
    try {
        GroovyObject goo = (GroovyObject) clz.newInstance();
        if (beanFactory != null) {
            beanFactory.autowireBeanProperties(goo, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true);
        }
        return cls.cast(goo);
    } catch (Exception e) {
        throw new RuntimeException("腳本實例化失敗: " + scriptName, e);
    }
}

緩存清理機制:為保持腳本修改後生效,需提供緩存刷新功能:

  • 手動觸發:管理端點調用 scriptClassCache.clear()。
  • 定時觸發:配合腳本文件修改時間戳檢查,定期失效過期緩存。

經驗總結

  • 動態語言特性(如 Groovy、JavaScript)在 Java 中使用時,務必注意類加載器洩漏風險。
  • Spring 的 autowireBeanProperties 會將對象納入容器生命周期管理,若對象頻繁創建且非單例,會造成內存壓力。
  • 理解類加載機制、GC 根可達性分析是排查 Metaspace/OOM 的關鍵。

此案例再次驗證:「不了解框架內部實現,輕易調用底層 API,必踩坑。」

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved