記一次內存溢出的經歷

       內存溢出對於我們做開發的人來說肯定是聽說過的,但是對於java開發程序員想要遇到一次真正的內存溢出還挺不容易的。因爲java自己會有內存回收機制,所以我們一般都是分配好內存後只管使用,不管回收,不用擔心內存的問題。而這次居然讓我碰上了一次。可得好好記錄一下。

        首先問題的表象是這樣的。項目中有一個服務是提供了前端報表頁面的數據查詢統計功能,而這個服務後來發現一直在啓動後不久就會掛掉,然後就連接不上zookeeper註冊中心。重啓服務又可以連接上了。剛開始還以爲是zookeeper集羣的問題。後來通過xshell的top命令觀察內存的時候發現在服務啓動後,內存使用一直不停的在變多,一直飆升到5G。而聯繫運維同事得知,該服務的內存只分配了2G,難道是項目需要的內存不夠?導致JVM沒有內存?這時候開始仔細觀察日誌,發現了重要點。

11-21 00:00:51.058 [ERROR] [pool-4-thread-1] org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
java.lang.OutOfMemoryError: GC overhead limit exceeded
	at redis.clients.jedis.Protocol.processBulkReply(Protocol.java:181)
	at redis.clients.jedis.Protocol.process(Protocol.java:158)
	at redis.clients.jedis.Protocol.processMultiBulkReply(Protocol.java:209)
	at redis.clients.jedis.Protocol.process(Protocol.java:160)
	at redis.clients.jedis.Protocol.read(Protocol.java:218)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:341)
	at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:277)
	at redis.clients.jedis.BinaryJedis.hgetAll(BinaryJedis.java:1137)
	at redis.clients.jedis.BinaryJedisCluster$48.execute(BinaryJedisCluster.java:555)
	at redis.clients.jedis.BinaryJedisCluster$48.execute(BinaryJedisCluster.java:552)
	at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:114)
	at redis.clients.jedis.JedisClusterCommand.runBinary(JedisClusterCommand.java:57)
	at redis.clients.jedis.BinaryJedisCluster.hgetAll(BinaryJedisCluster.java:557)
	at com.*.framework.cache.JedisClusterImpl.getMapValues(JedisClusterImpl.java:257)
	at com.*.scheduleTask.EmailAlertTask.*(EmailAlertTask.java:431)

     這個時候發現這個JVM的GC因爲內存不夠所以已經GC失敗了。所以我們可以知道應該是內存被應用消耗完了。這時候我們去看JVM的dump文件發現裏面的某一個對象所佔的空間特別大。有一百多萬個,而這個對象是剛好對應着數據庫的一張表。這張表的屬性很多,每個屬性值的內容也不少。

     檢查代碼才發現,原來這裏有一個方法涉及到了統計的功能,在這一個方法中需要把這種表中所有的對象查出來,大概1萬2千個,而根據業務查了3次,導致每個方法會產生3萬6個大對象。前端也在一直輪詢查詢,30秒一次。這樣下來經過換算,14分鐘就會達到上百萬個對象。這時候可能會有疑惑,爲什麼這裏的內存沒有回收呢?我猜想是因爲這裏的方法響應慢,來不及觸發JVM的GC,GC的垃圾回收算法都是以空間或時間作爲緯度來觸發,這裏先不深究。那我們怎麼解決呢?

     從上層解決。該方法調用的是dubbo的接口方法。而dubbo的provider有線程池的配置,我們可以通過修改dubbo的線程池配置,從程序的上游來解決內存中對象多的問題,相當於控制源頭,不讓生產出那麼多對象。一般dubbo的線程池配置是這樣的

<dubbo:provider retries="5" timeout="60000" loadbalance="leastactive" executes="2000" threads="2000" actives="2000"/>

其中executes設置爲2000。說明可以創建2000個線程。這樣來一個請求就會創建一個線程,線程就會生成新的對象。所以我們把這裏的值設置爲50或者100。這個看項目大小情況。這樣就發現內存降下來了。很意外吧。沒想到居然通過dubbo調優的的方式解決了這個問題。以下是總結:

       在發生內存溢出的事件後,我們可以先加大內存(如果有條件的話),雖然通常是沒有用的,哈哈。但是也可以增加我們的觀察時間。當還沒有找到問題的時候,可以對服務進行監控(做一個心跳接口)。看一下大概服務掛掉的時間點。看看有沒有規律。這些都有助於我們排查問題。也可以在服務準備掛掉的時候重啓,爭取不影響線上的服務(前提是服務有做負載均衡)。這些都是緊急的一些處理方法。

       分析原因的時候,我們需要從自己的代碼中分析哪裏會導致內存被消耗,這裏可以通過應用的日誌,方法的調用頻率等其他角度來分析,而分析JVM的日誌文件也是一個排查問題的辦法。因爲內存溢出原理說白了就是內存不夠用了。然後再根據自己應用的實際情況去選擇處理的方式。JVM底層的垃圾回收機制雖然也是一個解決的方向,但就普通的應用而言,還遠遠沒到需要修改這裏的程度。所以只需要查看GC的回收算法是否合理就可以了。最終還是要回歸到自己寫的代碼中去。看看是不是自己生成的對象太多了,避免一些沒有必要的內存浪費。然後如果是微服務架構的項目,可以從使用的RPC框架去看看有沒有調優的地方。

        時刻準備着,說不定下一個內存溢出就會找上你哦~哈哈

 

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