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


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

文章插图
 
Unsafe.allocateMemory的使用场景有两个:第一 , 封装在DirectByteBuffer内;第二 , 业务直接使用Unsafe.allocateMemory 。
DirectByteBuff通常被用于通信框架如netty中 , 不仅可以减少GC压力 , 而且避免IO操作时将对象从堆上拷贝到堆外 。为了快速验证是否DirectByteBuffer导致内存泄露 , 可使用参数-XX:MaxDirectMemorySize限制DirectByteBuffer分配的堆外内存大小 , 如果堆外内存仍然大于MaxDirectMemorySize , 可基本排除DirectByteBuffer导致的内存泄露 。
分析DirectByteBuffer的内存首先可用Java Mission Control , 绑定到进程 , 并查看DirectByteBuffer占的内存如2.24GB 。此处也可直接用MemoryAnalyzer打开dump的堆 , 统计所有DirectByteBuffer的capacity之和 , 计算DirectByteBuffer申请的堆外内存大小 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
然后用命令jdk/bin/jmap -dump:live,format=b,file=heap.hprof {pid} , 导出堆里所有活着的对象 , 并用MemoryAnalyzer打开dump的堆 , 分析所有的DirectByteBuffe:Merge shortest path to GC Roots ->with all references 。
如果排除DirectByteBuffer , 那就是应用程序直接用Unsafe类的allocateMemory分配的内存 , 例如Spark的off heap memory[1] 。此时可排查代码所有Unsafe.allocateMemory的地方 。
Java调用C++组件例如RocksDB采用C++实现 , 并通过JNI提供给Java调用的接口 , 如果Java通过JNI创建了新的RocksDB实例 , RocksDB会启动若干后台线程申请、释放内存 , 这部分内存都对Java不可见 , 如果发生泄漏 , 也无法通过dump jvm堆分析 。
【Java内存泄漏、性能优化、宕机死锁的N种姿势】分析工具可采用google的gperftools , 也可用jemalloc , 本文采用jemalloc , 首先安装jemalloc到/usr/local/lib/libjemalloc.so 。
git clone https://github.com/jemalloc/jemalloc.gitgit checkout 5.2.1./configure --enable-prof --enable-stats --enable-debug --enable-fillmake && make install然后在进程启动脚本里 , 添加如下命令 , LD_PRELOAD表示JVM申请内存时不再用glibc的ptmalloc , 而是使用jemalloc 。MALLOC_CONF的lg_prof_interval表示每次申请2^30Byte时生成一个heap文件 。export LD_PRELOAD=/usr/local/lib/libjemalloc.soexport MALLOC_CONF=prof:true,lg_prof_interval:30并在进程的启动命令里添加参数-XX:+PreserveFramePointer 。进程启动后 , 随着不断申请内存 , 会生成很多dump文件 , 可把所有dump文件通过命令一起分析:jeprof --show_bytes --pdf jdk/bin/java *.heap > leak.pdf 。
leak.pdf如下所示 , 可看到所有申请内存的路径 , 进程共申请过88G内存 , 而RocksDB申请了74.2%的内存 , 基本确定是不正常的行为 , 排查发现不断创建新的RocksDB实例 , 共1024个 , 每个实例都在运行 , 优化方法是合并RocksDB实例 。
需要注意的是 , 88G是所有申请过的内存 , 包含申请但已经被释放的 , 因此通过该方法 , 大部分情况下能确定泄露源头 , 但并不十分准确 , 准确的方法是在C++代码里用钩子函数勾住malloc和free , 记录哪些内存未被释放 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
性能优化arthasperf是最为普遍的性能分析工具 , 在Java里可采用阿里的工具arthas进行perf , 并生成火焰图 , 该工具可在Docker容器内使用 , 而系统perf命令在容器里使用有诸多限制 。
下载arthas-bin.zip[2] , 运行./a.sh , 然后绑定到对应的进程 , 开始perf: profiler start , 采样一段时间后 , 停止perf: profiler stop 。结果如下所示 , 可看到getServiceList耗了63.75%的CPU 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
另外 , 常用优化小建议:热点函数避免使用lambda表达式如stream.collect等、热点函数避免使用正则表达式、避免把UUID转成String在协议里传输等 。
jaegerperf适用于查找整个程序的热点函数 , 但不适用于分析单次RPC调用的耗时分布 , 此时就需要jaeger 。


推荐阅读