Java 服務內存佔用過高的一次排查過程

1. 緣由

日常敲代碼時,運維同事突然把小組人員都拉進了一個羣裏,說一臺線上機器內存耗盡,OOM 導致服務註冊的 Mesh 客戶端被幹掉了,部分服務調用異常。運維同事查看機器負載,發現我們組內一個Java 服務佔用的內存有點異常,啓動命令-Xmx128m 指定了最大堆內存只有 128M,但是整個進程佔用的內存達到了 640M,顯然是有問題的

2. 線上排查

運維截圖一扔,鍋是甩不掉的,老老實實登錄到線上機器排查。內存佔用過高首先想到的就是發生了內存泄露,使用 Jmap -histo $pid > heap.log 輸出堆內對象統計情況到文件中,查看文件發現堆中佔用內存最多的是各種數組,沒有發現明顯的問題。沒法子,使用 top -H p $pid命令檢查該進程內運行的線程狀況,終於發現了可疑點,在這個Java 服務裏面運行的子線程居然有 5000 個,並且幾乎全部都在 Sleeping 狀態
在這裏插入圖片描述
這種情況首先想到的是發生了線程死鎖,資源爭用導致大量線程被阻塞了。使用 jstack -l $pid > stack.log 將線程棧相關狀況輸出到文件中,打開文件一搜索卻大失所望,根本沒有死鎖發生。線程狀態大都在 TIMED_WAITING,不過隨着一行行往下看,也發現了一個可疑點,以下這種 OkHttp ConnectionPool 的線程出現得太多了,線程序號甚至達到了1082707

"OkHttp ConnectionPool" #1082707 daemon prio=5 os_prio=0 tid=0x00007f564c18f000 nid=0x1a4d in Object.wait() [0x00007f5602cb4000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        at java.lang.Object.wait(Object.java:460)
        at okhttp3.ConnectionPool$1.run(ConnectionPool.java:67)
        - locked <0x00000000fc30fb30> (a okhttp3.ConnectionPool)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - <0x00000000fc305f98> (a java.util.concurrent.ThreadPoolExecutor$Worker)

此時回過頭來看堆內存文件,發現以下記錄,okhttp3.ConnectionPool 這個連接池對象實例佔用的內存雖然不多,只有300K,但是實例總數居然有 7876 個,毫無疑問是有問題的

  40:          7876         315040  okhttp3.ConnectionPool

3. 代碼排查

可疑點在於 ConnectionPool 這個對象的數量,在項目中搜索 ConnectionPool 對象初始化調用點,發現 OkHttpClient 初始化時會調用 ConnectionPool 的構造方法,也就是每一個 OkHttpClient 實例被創建出來都會伴隨着一個連接池 ConnectionPool的創建。而在項目代碼中,每次進行 RPC 調用的時候都會重新創建一個 OkHttpClient 對象,至此一切豁然開朗

  • 原因剖析
    OkHttpClient 對象在使用過後會被 JVM 回收,但是 OkHttp源碼中ConnectionPool的構造方法裏默認最大線程空閒數是5,keepAlive 時間爲5分鐘,也就是發起一次網絡連接後,5分鐘內不會斷開連接。這樣當OkHttpClient 對象被 JVM 回收時,ConnectionPool 因爲有線程還保持着與服務端的連接,處於Active狀態,5 分鐘內不會被回收,自然也不會釋放線程資源。線程會佔用內存空間,這樣隨着時間積累,線程數量越來越多,進程佔用的內存自然也越來越多了
    /**
    * Create a new connection pool with tuning parameters appropriate for a single-user application.
    * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
    * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
    */
    public ConnectionPool() {
     this(5, 5, TimeUnit.MINUTES);
    }
    

4. 解決方法

問題根源找到了,修復自然簡單。項目代碼中每次進行 RPC 調用都重新創建一個 OkHttpClient 對象,這實際上是極大的性能浪費,只要在代碼中保存一個靜態 OkHttpClient 對象,每次網絡請求都複用這個對象就可以了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章