Java内存泄漏、性能优化、宕机死锁的N种姿势

导读本文介绍JAVA诸多优化实例:第一 , 排查堆上、堆外内存泄露;第二 , 使用arthas、jaeger、tcpdump、jstack做性能优化;第三 , 排查进程异常退出的原因 , 如被杀、System.exit、Java调用的C++发生Crash、Java内Crash;第四 , 排查死锁的原因 , 如log4j死锁、封装不严谨导致的死锁
内存泄漏内存泄露在C++里排查很简单 , 用钩子函数勾住内存分配和释放函数malloc和free , 统计哪些malloc的内存没有free , 就可以找出内存泄露的源头 。但在Java里问题复杂的多 , 主要因为Java在内存之上有层JVM管理内存 。
JVM先从操作系统申请大内存 , 接着自己管理这部分内存 。所以Java程序的内存泄露分为两种:堆上内存泄露、堆外内存泄露 , 而堆外内存泄露又分为两种:Java使用堆外内存导致的内存泄露、Java程序使用C++导致的内存泄露 。
分析内存泄露首先需要确认是堆上泄漏还是堆外泄露 。可以通过jmap -heap pid确认 , 如下图所示 , 老年代PS Old Generation使用率占99.99% , 再结合gc log , 如果老年代回收不掉 , 基本确认为堆上内存泄露 , 也不排除进程本身需要这么多内存 , 此时需要分析堆 。而堆外内存泄露的显著表现是top命令查出来的物理内存显著比通过xmx配置的最大内存大 。

Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
堆上内存泄漏堆上内存泄露是最常见的 , 申请的对象引用和内存全在JVM堆上 , 而对象使用完后 , 对象引用被其他长生命周期的对象一直拿着 , 导致无法从堆上释放 。首先用jdk/bin/jmap -dump:live,format=b,file=heap.hprof {pid} , 导出堆里所有活着的对象 。然后用工具分析heap.hprof 。
分析堆上内存泄露的主流工具有两种:JDK自带的bin目录下的jvisualvm.exe、Eclipse的MemoryAnalyzer 。MemoryAnalyzer更强大 , 可自动分析可疑的内存泄露 。使用MemoryAnalyzer时 , 需要在MemoryAnalyzer.ini里通过-Xmx参数配置最大内存 , 否则无法打开大堆 。接下来介绍堆上内存泄露的若干实例 。
对象被静态对象引用使用MemoryAnalyzer自动分析内存泄露 , 报告如下 , 可以看到RaftServerMetrics占了44.68%的内存 , 所有实例大小98M内存 , 且所有的RaftServerMetrics实例被一个ConcurrentHashMap引用 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 

Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
接着在直方图里过滤RaftServerMetrics , 共找到2065个实例 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
然后右键RaftServerMetrics->Merge shortest path to GC Roots ->with all references查找所有引用RaftServerMetrics的地方 , 结果如下 , 可看到所有的RaftServerMetrics实例被变量metricsMap引用 , 问题原因是RaftServerMetrics使用完后 , 未从静态变量metricsMap里删除 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
RPC连接使用完后未关闭MemoryAnalyzer自动分析内存泄露时 , 有时并不能准确的找到 , 此时需要自己分析哪些对象占用内存过多 。下图是使用jvisualvm.exe打开堆的结果 , 查看数目或者内存异常的对象 , 可以看到很多对象数目都是111580个 , 且最后一列显示的内存占用大 , 从对象的包分析 , 都和netty有关 , 且是client相关的对象 , 基本确认这些对象和内存泄露有关 。进一步分析代码 , 发现大量RPC连接使用完后未关闭 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
堆外内存泄露Java使用堆外内存JDK提供绕过JVM直接在操作系统申请内存的接口 , 例如通过Unsafe类的allocateMemory、freeMemory直接分配、释放内存 , 内存对象的引用在堆上 , 但内存在堆外 。排查此类内存泄露 , 首先开启:
-XX:NativeMemoryTracking=detail
然后jcmd pid VM.native_memory detail , 打出内存分配信息 , 注意NativeMemoryTracking显示的内存不包含C++分配的内存 。此处需要关注两个点 , 第一 , Total行的committed数值是否等于进程占用的物理内存 , 如果不等 , 说明有C++等native code分配的内存 , 可参考Java调用C++组件 分析;第二 , Native Memory Tracking的committed数值是否过大 , 如果过大 , 说明有Unsafe.allocateMemory分配了太多内存 。


推荐阅读