JVM 內存泄漏分析

初始代碼

public class App {

    private static final Map<Long, Object> productCache = new HashMap<>();

    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        try {
            while (true) {
                long id = System.nanoTime(); 
                productCache.put(id, new Object());
                
                if (productCache.size() % 1_000_000 == 0) {
                    System.out.println("The current number of cached objects: " + productCache.size());
                }
            }
        } catch (OutOfMemoryError e) {
            System.err.println("\n OOM!!! The final number of cached objects: " + productCache.size());
            e.printStackTrace();
        }
    }
}

編譯運行

命令行在APP.java所在目錄執行

# 編譯
javac App.java

# 運行,最大堆內存大小為 256MB,自動生成快照
java -Xms128m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/tmp/demo1-heap.hprof App

分析內存快照

使用MAT1.7.0版本,MAT入門參考

Java 內存泄漏原因、解決辦法及泄漏排查_緩存


Java 內存泄漏原因、解決辦法及泄漏排查_緩存_02

修復代碼

改用帶淘汰機制的緩存替換 HashMapLinkedHashMap 並實現 LRU(最近最少使用)淘汰策略,限制緩存最大容量:

public class App {

    // private static final Map<Long, Object> productCache = new HashMap<>();

    private static final Random RANDOM = new Random();

    private static final Map<Long, Object> productCache = new LinkedHashMap<Long, Object>(1024, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Long, Object> eldest) {
            // More than one million goods will be eliminated if they have not been used for the longest time
            return size() > 1_000_000;
        }
    };

    public static void main(String[] args) {
        try {
            while (true) {
                long id = System.nanoTime(); 
                productCache.put(id, new Object());
                
                if (productCache.size() % 1_000_000 == 0) {
                    System.out.println("The current number of cached objects: " + productCache.size());
                }
            }
        } catch (OutOfMemoryError e) {
            System.err.println("\n OOM!!! The final number of cached objects: " + productCache.size());
            e.printStackTrace();
        }
    }
}

再次編譯運行發現緩存維持在1_000_000

常見內存泄漏場景及解決

根據分析結果,針對性修復:

靜態集合未清理

問題:static List/Map 等容器長期持有對象引用,未及時移除(如緩存未設置過期策略)。
解決:改用弱引用集合(WeakHashMap)、設置緩存淘汰機制(如 LRU),或定期清理無效數據。

資源未關閉

問題:數據庫連接、文件流、網絡連接等資源未關閉,導致對象無法回收。
解決:使用 try-with-resources 自動關閉資源,或在 finally 塊中顯式釋放。

監聽器 / 回調未移除

問題:註冊的監聽器(如 GUI 事件、觀察者模式)未註銷,導致被監聽對象長期引用。
解決:在對象銷燬前移除監聽器,或使用弱引用實現回調。

線程泄漏

問題:線程池核心線程持有大對象引用,或未正確停止的線程(如 Thread 未中斷)。
解決:控制線程池核心線程數,使用 ThreadLocal 時注意清理(remove()),確保線程能正常終止。

類加載器泄漏

問題:自定義類加載器加載的類未被回收(如熱部署場景),導致元空間溢出。
解決:避免長期持有類加載器引用,確保加載的類及其依賴可被 GC 回收。