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


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

文章插图
 
接下来使用命令gdb bin/java core.44729打开core文件 , 发现是rocksdb start thread时挂的 , 挂在libstdc++里 , 这是glibc库 , 基本不可能出问题 , 因此该堆栈可能是表象 , 有其他原因导致start thread失败 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
注意到打开core文件时 , 有太多线程-LWP轻量级进程 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
然后在gdb里用info threads , 发现有三万多个线程 , 都在wait锁状态 , 基本确认三万多个线程 , 导致内存太大 , 创建不出来新的线程 , 因此挂在start thread里 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
接着分析三万多个线程都是什么线程 , 随机选几十个线程 , 打出每个线程的堆栈 , 可以看到大部分线程都是jvm线程 。因为rocksdb创建出来的线程是:
从/tmp/librocksdbjni8646115773822033422.so来的;而jvm创建出来的线程都是从/usr/java/jdk1.8.0_191-amd64/jre/lib/amd64/server/libjvm.so来的 , 这部分线程占了大部分 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 

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

文章插图
 
因此问题出在Java代码里 , 产生core.pid文件的进程 , 虽然没有产生crash log , 但也是因为Java 线程太多 , 导致C++代码创建线程时挂掉 。至于为什么Java线程太多请看Java内Crash 。
另外 , core.pid完整的保留了C++组件Crash时的现场 , 包括变量、寄存器的值等 , 如果真的因为C++组件有Bug而Crash , 例如空指针等 。首先自行找到C++源码 , 找出怀疑空指针的变量{variableName} , 通过在gdb里执行命令:p {variableName} , 可以看出每个变量的值 , 从而找出空指针的变量 。
Java内Crash排查Java内Crash的原因如OOM等 , 需要配置JVM的如下参数:
-XX:ErrorFile
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath 。
JVM内发生Crash时 , 会在-XX:ErrorFile配置的路径下生成crash log 。而-XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath用于发生OOM时生成Dump堆 , 用于还原现场 。下图所示为产生的crash log 。可以看到创建线程时发生OutOfMemory导致进程挂掉 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
从下图crash log可以看到有两万四千个Datanode State machine Thread线程都在等锁 。到此确认上文Java调用C++发生Crash 产生core.pid的进程和产生crash log的进程都是因为两万多个Datanode State Machine Thread挂掉 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
接着分析为何有两万多个Datanode State Machine Thread , 代码里可以看到该线程用线程池newCacheThreadPool创建 。该newCacheThreadPool在没有线程可用 , 例如线程都在等锁的情况下 , 会创建新的线程 , 因此创建了两万多个线程 。接着分析Datanode State Machine Thread等的什么锁 。在进程的线程数超过5000时 , 用jstack -l pid > jstack.txt打出所有线程的状态 。
可以看到几乎所有Datanode State Machine Thread在等锁 , 而只有一个Datanode State Machine Thread – 5500 拿到了锁 , 但是卡在提交RPC请求submitRequest 。至此Java调用C++发生Crash 和Java内Crash的原因找到 。
Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 

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

文章插图
 
死锁log4j导致的死锁jstack打出的死锁信息如下所示 。grpc-default-executor-14765线程拿到了log4j的锁 , 在等RaftServerImpl的锁;grpc-default-executor-14776线程拿到了RaftServerImpl的锁 , 在等log4j的锁 , 导致这两个线程都拿到了对方等待的锁 , 所以造成两个线程死锁 。可以看出 , 仅仅打日志的log4j , 不释放锁是最值得怀疑的地方 。最后发现log4j存在死锁的缺陷[4] 。该缺陷在log4j2得到解决 , 升级log4j即可 。


推荐阅读