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

Proxool 連接池 HouseKeeper 線程關閉失敗導致內存洩漏警告根因分析

     編輯:Java 綜合教學

Proxool 連接池 HouseKeeper 線程關閉失敗導致內存洩漏警告根因分析

一、 現象:Tomcat 關閉時拋出嚴重警告

警告: The web application [ROOT] appears to have started a thread named [HouseKeeper] but has failed to stop it. This is very likely to create a memory leak.
Stack trace of thread:
 java.lang.Thread.sleep(Native Method)
 org.logicalcobwebs.proxool.HouseKeeperThread.run(HouseKeeperThread.java:46)

二、 源碼追溯:關閉流程與 HouseKeeperThread 實現

Proxool 關閉入口:ProxoolFacade.shutdown() → 關閉所有連接池 → HouseKeeperController.shutdown()。

HouseKeeperController.shutdown()

protected static void shutdown() {
    synchronized(LOCK) {
        Iterator i = houseKeeperThreads.iterator();
        while (i.hasNext()) {
            HouseKeeperThread hkt = (HouseKeeperThread) i.next();
            LOG.info("Stopping " + hkt.getName() + " thread");
            hkt.cancel(); // 僅設置 stop = true
        }
        houseKeeperThreads.clear();
    }
}

HouseKeeperThread.run() 核心邏輯

public class HouseKeeperThread extends Thread {
    private boolean stop; // 關鍵缺陷:非 volatile!
    
    public void run() {
        while (!stop) {
            HouseKeeper hk = HouseKeeperController.getHouseKeeperToRun();
            while (hk != null && !stop) {
                try { hk.sweep(); } catch (ProxoolException e) { LOG.error(...); }
                hk = HouseKeeperController.getHouseKeeperToRun();
            }
            try {
                Thread.sleep(5000); // 每 5 秒掃描一次
            } catch (InterruptedException e) {
                LOG.error("Interrupted", e);
            }
        }
    }
    
    protected void cancel() {
        stop = true; // 普通寫入,無內存屏障
    }
}

三、 核心缺陷:stop 字段缺少 volatile 修飾

問題後果
private boolean stop; 非 volatileJIT 編譯器可能將 while (!stop) 優化為 if (!stop) while(true) (循環不變量外提),導致 cancel() 修改對運行線程**不可見**,線程永不退出。
sleep(5000) 期間收到關閉請求線程處於阻塞態,cancel() 設置 stop=true 後,線程醒來捕獲 InterruptedException,但 catch 塊僅記錄錯誤,**未檢查 stop 標誌**,循環繼續執行。
無 interrupt() 喚醒shutdown() 僅調用 cancel(),**未調用 thread.interrupt()** 打斷睡眠,線程需等待 5 秒自然醒來才能檢查 stop。

四、 為什麼 Tomcat 仍報警「failed to stop it」?

  1. Tomcat 關閉 Web 應用時,調用 ServletContextListener.contextDestroyed() → 應用調用 ProxoolFacade.shutdown()。
  2. HouseKeeperController.shutdown() 遍歷調用 hkt.cancel() 設置 stop=true。
  3. 此時 HouseKeeper 線程**正處於 sleep(5000) 中** (或剛進入下一輪循環)。
  4. 由於無 interrupt(),線程**無法立即醒來**檢查 stop 標誌。
  5. Tomcat 等待線程結束超時 (默認幾秒),判定線程未正常停止,拋出警告。
  6. 即使後續線程醒來,因 stop 非 volatile,可能**永遠看不到 true**,導致真正的內存洩漏 (線程永存,持有類加載器引用,阻礙 Metaspace/Heap 回收)。

五、 連接池本身已正確關閉 (日誌證據)

[INFO] Shutting down 'autumn' pool immediately
[DEBUG] #0006 removed because of shutdown
[DEBUG] Connection #6 closed
...
[INFO] 'autumn' pool has been closed down ... in 6 milliseconds
[INFO] Stopping Prototyper thread
[INFO] Stopping HouseKeeper thread  <-- 此處調用 cancel(),但線程未實際終止

連接池資源釋放無誤,**唯一隱患是 HouseKeeper 守護線程未終止**。

六、 修復方案 (若必須繼續使用 Proxool)

方案 1:反射修補 stop 字段為 volatile (不推薦,侵入性強)

方案 2:自定義 ShutdownHook 強制 interrupt HouseKeeper 線程

// 在 ServletContextListener.contextDestroyed() 中,調用 ProxoolFacade.shutdown() 後補充:
try {
    Field f = HouseKeeperController.class.getDeclaredField("houseKeeperThreads");
    f.setAccessible(true);
    @SuppressWarnings("unchecked")
    List threads = (List) f.get(null);
    for (HouseKeeperThread hkt : threads) {
        hkt.interrupt(); // 打斷 sleep,強制檢查 stop
        hkt.join(2000);  // 等待最多 2 秒
    }
} catch (Exception e) {
    log.error("強制停止 HouseKeeper 失敗", e);
}

方案 3 (根治):遷移至成熟連接池 — HikariCP / Druid / Tomcat JDBC Pool

  • Proxool 最後更新於 2013 年,**已停止維護 10 年+**,存在已知並發缺陷、無志願者修復。
  • HikariCP:零開銷、極高性能、主動連接驗證、無守護線程洩漏風險,Spring Boot 2.x+ 默認選擇。
  • Druid:阿里巴巴出品,監控強大、SQL 防火牆、兼容 Oracle/PostgreSQL 等方言,國內廣泛使用。

七、 總結與教訓

  1. volatile 是線程可見性的基石: 任何跨線程讀寫的標誌位**必須 volatile**,否則 JIT 優化會導致不可見。
  2. 線程停止協議: interrupt() + 標誌位 + join() 才是標準停止模式,單純設置 boolean 無法打斷阻塞操作。
  3. 守護線程 ≠ 安全退出: setDaemon(true) 僅意味 JVM 退出時不等待它,**應用容器熱部署/關閉時必須顯式停止**,否則類加載器洩漏。
  4. 技術選型要看維護狀態: 核心基礎設施 (連接池、日誌、RPC) 必須選擇活躍維護、社區成熟的組件,避免踩十年前的坑。

此警告雖不直接導致業務故障,但揭示了框架底層並發編程缺陷與技術債風險。生產環境強烈建議**完成連接池遷移**,徹底消除隱患。

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