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


分析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堆分析 。
分析工具可采用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 。
jaeger是Uber开源的一个基于Go的分布式追踪系统 。jaeger基本原理是:用户在自己代码里插桩,并上报给jaeger,jaeger汇总流程并在UI显示 。非生产环境可安装jaeger-all-in-one[3],数据都在内存里,有内存溢出的风险 。在需要追踪的服务的启动脚本里export JAEGER_AGENT_HOST={jaeger服务所在的host} 。
下图为jaeger的UI,显示一次完整的流程,左边为具体的插桩名称,右边为每块插装代码耗时,可以看到最耗时的部分在including leader create container和including follower create container,这部分语义是leader创建完container后,两个follower才开始创建container,而创建container非常耗时,如果改成leader和两个follower同时创建container,则时间减少一半 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
tcpdumptcpdump常用来抓包分析,但也能用来优化性能 。在我们的场景中,部署Ozone集群(下一代分布式对象存储系统),并读数据,结果发现文件越大读速越慢,读1G文件,速度只有2.2M每秒,使用perf未发现线索 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
用命令tcpdump -i eth0 -s 0 -A 'tcp dst port 9878 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420' -w read.cap,该命令在读200M文件时会将所有GET请求导出到read.cap文件,然后用wireshark打开read.cap,并过滤出HTTP协议,因为大部分协议都是TCP协议,用于传输数据,而HTTP协议用于请求开始和结束 。


推荐阅读