轉 從一個OutOfMemoryError 學會了分析Java記憶體洩漏問題
閱讀目錄
以前都是好好的,最近出現了 oom。
問題
View Code
開始是: java.lang.OutOfMemoryError: Java heap space
512M 不夠嗎? 很有可能啊...
增加記憶體到1G 後仍然出現問題:Failed to mark a promise as failure because it has failed already: [DefaultChannelPromise@33a99639(failure: io.netty.handler.codec.EncoderException: java.lang.OutOfMemoryError: GC overhead limit exceeded), io.netty.handler.codec.EncoderException: java.lang.OutOfMemoryError: GC overhead limit exceeded
這就奇怪了! 注意到 出現次數比較多是 com.lkk.platform.system.domain.service.ELCommonCodeRegulationService, method: GetCode,
@Transactional(readOnly = false) public String GetCode(String name){ RLock rlock = redissonManager.getRedisson().getLock(name); boolean getLock = false; try{ getLock = rlock.tryLock(3, 20, TimeUnit.SECONDS); if (getLock){ ELCodeDef elCodeDef = findCommonCode(name); super.updateById(elCodeDef); return elCodeDef.getCode(); } }catch (Exception ex){ ex.printStackTrace(); }finally { if (getLock) { rlock.unlock(); } } return ""; }
而 @Autowired RedissonManager redissonManager;
分析
由此懷疑這個地方有些問題。 雖然出現了oom, 但是程序沒有死, 似乎依然可以響應某些請求,於是把執行緒dump 下來, 觀察一番,發現redisson-netty竟然有上千個
就是這個
"redisson-netty-25-32" #808 prio=5 os_prio=0 tid=0x00007f7ec0187800 nid=0x3625 runnable [0x00007f7e77d6c000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0x00000000e90b27a0> (a io.netty.channel.nio.SelectedSelectionKeySet) - locked <0x00000000e90b27f8> (a java.util.Collections$UnmodifiableSet) - locked <0x00000000e90b2708> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at io.netty.channel.nio.SelectedSelectionKeySetSelector.select(SelectedSelectionKeySetSelector.java:62) at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:786) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:434) at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.lang.Thread.run(Thread.java:748)
太不正常了! 但是 這裡的redisson-netty- 仍然是RUNNABLE狀態, 看起來也不是問題啊! 仔細檢查了下, 也沒發現死鎖啊!!
那就不是執行緒問題嗎?
redisson 的bug 嗎? redisson 的官網 的issue 搜尋一番,無果。 鬱悶了! 而且我的 redisson 版本是 3.1.1, 已經很新的了吧!!
堆疊分析吧!!把java 的heap 拔下來,
jps -l, 然後jmap-dump:format=b,file=dumpFileName pid
看到有些異常:
肯定不是 spring 的classloader 吧。
看到 netty 的PoolThreadCache 比較可疑啊, 還有 mybatis。
Biggest Top-Level Dominator Packages 跟之前一樣的提示, 一個是netty的 PollThreadCache, 一個是netty 的epoll, 還有是redision, 還有是sun 的EPollArrayWrapper, 還有mybatis,其他 也看不出什麼來啊!
分析只能到此為止了嗎? io.netty.buffer.PoolThreadCache 是什麼東東? 我不熟悉啊! 看過netty 原始碼, 已經全忘了!
是記憶體洩漏嗎? 好像也看不出來。 不太確定。 網上搜索看看吧!!
還是從redision 入手吧。 咦, redision 的用法好像不太對哦!!! 改一下吧:
@Autowired RedissonClient redissonClient; ==> @Autowired RedissonManager redissonManager; RLock rlock = redissonClient.getLock(name); ==> RLock lock = redissonManager.getRedisson().getLock(name);
而RedissonManager如下:
import org.apache.commons.lang3.StringUtils; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.redisson.config.ReadMode; import org.redisson.config.SentinelServersConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Component public class RedissonManager { @Autowired RedisTemplate<String, Object> redisTemplate; @Value("${spring.redis.password}") private String redisPassword; @Value("${spring.redis.port}") private String redisPort; @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.timeout}") private String redisTimeout; @Value("${spring.redis.sentinel.node}") private String redisSentinelNode; @Value("${spring.redis.sentinel.master}") private String redisSentinelMaster; @Bean public RedissonClient getRedisson() { Config config = new Config(); if (StringUtils.isNotEmpty(redisPort)) { config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort).setPassword(redisPassword); } else if (StringUtils.isNotEmpty(redisSentinelNode)) { String[] nodes = redisSentinelNode.split(","); List<String> newNodes = new ArrayList(nodes.length); Arrays.stream(nodes).forEach((index) -> newNodes.add(index.startsWith("redis://") ? index : "redis://" + index)); SentinelServersConfig serverConfig = config.useSentinelServers() .addSentinelAddress(newNodes.toArray(new String[0])) .setMasterName(redisSentinelMaster) .setReadMode(ReadMode.SLAVE) .setTimeout(Integer.valueOf(redisTimeout)); if(StringUtils.isNotEmpty(redisPassword)){ serverConfig.setPassword(redisPassword); } } return Redisson.create(config); } }
改了就好了!!突然自己明白了, 原來就是這個redision 用法錯誤導致的!!
不信? 重新拔下來heap dump 分析一下:
最大的 com.mysql.cj.jdbc.AbandonedConnectionCleanupThread 才佔用2m, 不是什麼問題。 可見已經沒有了什麼
PoolThreadCache 已經下滑到了第七位, 總佔用7M ,38個物件,看起來正常了許多!! :
總結
花了2天時間終於搞定!!
其實上面的 thread dump 和 heap dump 已經給出了比較明顯的答案了!! 就是 PoolThreadCache 佔用了 過多的記憶體, 其原因就是 PoolThreadCache 錯誤的建立了 太多!———— 本來應該是單例的 物件, 被搞成了 prototype, 你說是不是引起了大錯!!! 一個 PoolThreadCache佔用記憶體差不多196,000byte, 921個就 是 180516000 byte 也就是 差不多 下圖的180M, 一類物件就 180M, 總共才1G, 當然會不夠用!!
其實 從錯誤日誌也可以 分析出來一些, 在建立需要比較大的記憶體的物件的時候, 就會出現 oom, 因為記憶體確實已經不夠了啊!! (這也是為什麼 ELCommonCodeRegulationService 的 GetCode 方法呼叫的時候,出現了很多oom。 但是又不是絕對的。 因為其他 地方也可以建立大記憶體物件)
其實只要再多問幾個問題就知道了答案: 這個物件為什麼出現了這麼多次, 佔用這麼多記憶體呢?? 這個是正常的嗎? 如果能夠很早認識到這些問題,並回答之, 那麼問題就不是大問題了,就不會浪費很多時間了!