如何快速定位JVM相关GC问题

1.涉及问题

  • 如何使用jstat 命令查看jvm的GC情况
  • 面对海量GC日志,如何快速抓住问题根源- 如何使用日志分析工具
  • 对代码优化、扩容、参数调优、内存估算,都需要一些支撑信息加以判断
    如何快速定位JVM相关GC问题_第1张图片

GC 日志输出

GC版本差异

  • java版本更新速度很快,jvm参数配置变化较大,如java9几乎是推翻重来。追踪、定位GC日志之前需确认java版本- 事故出现时间不定,需要保留事故出现现场,可通过设置一些JVM参数进行保留相关信息

java8

  • jdk8 具体使用例子
    #! /bin/sh
    # 使用将下面添加注释去掉(markdown语法需要转义#,在使用时需要将转义符去掉)
    
    # GC 日志存放目录
    LOG_DIR=”/tmp/logs” 
    
    # 打印GC日志
    JAVA_OPT_LOG=”-verbose:gc” 
    
    # 打印GC详细信息
    JAVA_OPT_LOG=”${JAVA_OPT_LOG} -XX:+PrintGCDetails” 
    
    # 打印GC日志带系统时间
    JAVA_OPT_LOG=”${JAVA_OPT_LOG} -XX:+PrintGCDateStamps” 
    
    # 打印应用停顿(STW)时间
    JAVA_OPT_LOG=”${JAVA_OPT_LOG} -XX:PrintGCApplicationStoppedTime”
    
    # 打印对象年龄分布,对调优MaxTenuringThreshold参数帮助很大
    JAVA_OPT_LOG=”${JAVA_OPT_LOG} -XX:+PrintTenuringDistribution” 
    
    #将以上GC内容输出到文件中,并指定gc日志文件的格式
    JAVA_OPT_LOG=”${JAVA_OPT_LOG} -Xloggc:${LOG_DIR}/gc_%p.log” 
    
    # 指定堆内存溢出相关的日志存储目录,Dump文件保存路径,历史错误日志文件路径及命名格式
    JAVA_OPT_OOM=” -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log” 
    
    JAVA_OPT=”${JAVA_OPT_LOG} ${JAVA_OPT_OOM}”
    
     # JVM缩简日志输出
    JAVA_OPT=”${JAVA_OPT} -XX:-OmitStackTraceInFastThrow”  
    

java13

  • java9 开始,移除40个GC日志相关参数,具体见JEP 158.java13实例如下
    #! /bin/sh 
    # GC日志存储父目录
    LOG_DIR=”/tmp/logs”   
    
    # 打印GC日志
    JAVA_OPT_LOG=” -verbose:gc”
    
    #将GC 引用、堆相关的debug日志输出到日志文件		   JAVA_OPT_LOG=”${JAVA_OPT_LOG} -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=${LOG_DIR}/gc_%p.log:tags,uptime,time,level” 
    
    # 将jvm安全点(用于锁降级机制)相关信息输出到日志文件JAVA_OPT_LOG=”${JAVA_OPT_LOG} -Xlog:safepoint:file=${LOG_DIR}/safepoint_%p.log:tags,uptime,time,level” 
    
    # 打印OOM信息,指定Dump日志文件路径,错误日志文件JAVA_OPT_OOM=” -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log” 
    
    # 配置JAVA_OPT_LOG及JAVA_OPT_OOMJAVA_OPT=”${JAVA_OPT_LOG} ${JAVA_OPT_OOM}” 
    
    # 对jvm输出日志进行缩简JAVA_OPT=”${JAVA_OPT} -XX:-OmitStackTraceInFastThrow” 
    
    # 打印jvm的最终配置参数
    Echo $JAVA_OPT
    
  • java13除了GC日志,还输出了safepoint日志(可以安全地暂停线程的点,用来实现锁的降级),当GC发生时,用户线程必须全部停止,才可以进行垃圾回收,我们称此为JVM是安全的(safe),整个堆是稳定的。
    如何快速定位JVM相关GC问题_第2张图片
  • 如果在GC之前,有线程迟迟无法进入safepoint,那么整个JVM都在等待这个阻塞(blocked)线程,进而会造成整体GC的实际变长- 因此,在GC进行之前无法进入safepoint也会挂起JVM(GC也会挂起JVM)。
  • 因此,一般项目中会输出详细GC日志,并加上可读性强的GC日志时间戳,特别情况下会加一些反映对象分布,晋升情况和堆详细信息,用来排查问题。同样,设置OOM时自动Dump堆栈

GC 日志的意义

GC日志参数及数值解析

  • 具体例子
    如何快速定位JVM相关GC问题_第3张图片

  • 上述例子解析

    • 步骤1,表示GC发生时机,一般使用可读方式打印
    • 步骤2,表示是G1的“转移暂停:混合模式”,停顿月223ms
    • 步骤3,表示由8个worker线程并行执行,消耗214ms
    • 步骤4,表示Diff越小越好,越说明每个worker线程速度都很均匀
    • 步骤5,表示外部根区扫描,外部根是堆外区。JNI(Java Native Interface)引用,JVM系统目录,ClassLoader等
    • 步骤6,表示更新RememberedSet的时间信息
    • 步骤7,表示该任务主要是对CSet中存活对象进行赋值
    • 步骤8,表示花在GC之外的工作线程的时间
    • 步骤9,表示并行阶段的GC总时间
    • 步骤10,表示其他清理活动
    • 步骤11,表示收集结果统计
    • 步骤12,表示时间花费统计

分析工具

  • 即使理解日志中上述每个阶段每个数值意义,分析问题时,面对大量日志信息也会吃力,需要使用图形化分析工具进行分析
  • time ls / 进行三种维度的时间统计
    如何快速定位JVM相关GC问题_第4张图片
    • real,实际花费时间,从开始到结束花费时间。如进行等待I/O完成,阻塞时间也会计入
    • user,进程在用户态(user Mode) 花费时间,只统计本进程所使用时间,是指多核
    • sys,进程内核态(kernel Mode)花费的CPU时间,指内核中系统调用花费时间,只统计本进程使用时间
    • 对于并行垃圾收集器,如上述GC日志,real < user + sys,因为我们使用多核垃圾收集,所以实际发生实际发生时间比(user+sys)少很多,这种情况多核机器很常见
    • 对于串行垃圾收集器,GC时始终仅使用一个线程,故 real = user+sys
    • 一般来说,用户关心系统停顿多久,对世界的影响时间非常关心,即real时间

GC 日志可视化

  • gceasy
    • 1.查看堆内存信息
      如何快速定位JVM相关GC问题_第5张图片
    • 2.关键信息,如吞吐量98.6%(一般超过95%就行),最大延迟:230ms,平均延迟42.8ms,延迟看服务接收程度(如SLA定义为50ms,则最大延迟有问题),下图服务接近99%的停顿在100ms以下,运行状态优秀。
      如何快速定位JVM相关GC问题_第6张图片
    • 3.看这些信息,一定结合宿主服务器监控去看,如GC发生期间,CPU会发生尖峰,就证明GC对CPU资源使用的有点多。多数情况下,如果吞吐和延迟在可接受范围内,这些对CPU的超额使用可接受(毕竟要极力压榨机器资源)
    • 4.交互式图表,对问题区域放大查看,图中表示垃圾回收后空间释放,可以看到效果比较好
      如何快速定位JVM相关GC问题_第7张图片
    • 5.G1的时间耗时,下图展示每个阶段花费时间。看到平均耗时最长阶段是Concurrent Mark阶段,由于是并发的,影响不大。随着时间推移,YoungGC竟然达到136485次,运行5天,花在GC时间的就2小时,比较可观。
      如何快速定位JVM相关GC问题_第8张图片
    • 6.其他,如下图,整个JVM创建100多T的数据,其中2.4TB被提升(promoted)到老年代,说明年轻代生命周期较久。另外还有safepoint的信息
      如何快速定位JVM相关GC问题_第9张图片
    • 7.有GC问题的图表样例
      • 停顿时间明显超长的GC问题
        如何快速定位JVM相关GC问题_第10张图片
      • 内存泄漏
        如何快速定位JVM相关GC问题_第11张图片
      • 上面问题都很明显。但大多数情况是,问题是偶发的。从基本衡量指标,就能考量整体的服务水准。如果这些没问题,就要看曲线的尖峰。
      • 一般来说,任何不平滑的曲线,都值得怀疑,那就需要看一下当时业务的具体情况。是用户请求突增引起,还是执行了批量定时任务,再或者查询了大批量数据,需要和一些服务监控一起才能定位出根本问题
    • 只靠GC来定位问题比较困难,我们只需要知道GC有问题就可以了。
    • jstat
      • gceasy等可视化工具,必须经历导出、上传、分析三个阶段,速度慢,无法向jstat进行实时查看堆内存- jstat输出例子,使用jstat -gcutil $pid 1000
    	   S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    	   0.00 0.00 72.03 0.35 54.12 55.72 11122 16.019 0 0.000 16.0190.00 
    	   0.00 95.39 0.35 54.12 55.72 11123 16.024 0 0.000 16.0240.00 
    	   0.00 25.32 0.35 54.12 55.72 11125 16.025 0 0.000 16.0250.00 
    	   0.00 37.00 0.35 54.12 55.72 11126 16.028 0 0.000 16.0280.00 
    	   0.00 60.35 0.35 54.12 55.72 11127 16.028 0 0.000 16.028
               说明:S0: 年轻代的survivor 0
               	S1: 年轻代的survivor 1
               	O: 年老代
               	M: MetaSpacke,即元空间(之前的方法区)
               	YGC:  年轻代回收次数
               	YGCT: 年轻代回收耗时
               	FGC:  FullGC回收次数
               	FGCT:  FullGC回收耗时 
    
    -gcutil位置的参数可有多种,最常见有gc,gcutil,gccause,gcnew
    jstat -gc $pid 1000 : 显示和GC相关堆信息
    jstat -gcutil $pid 1000 : 显示垃圾回收信息
    jstat -gccause $pid 1000 : 显示垃圾回收相关信息(同 -gcutil),同时显示最后一次或当前发生垃圾回收的诱因
    jstat -gcnew $pid 1000 : 显示新生代信息
    jstat -gccapacity $pid 1000 : 显示各个代的容量即使用情况
    jstat -gcmetacapacity $pid 1000 : 显示元空间metaspace的大小
    jstat -gcnewcapacity $pid 1000 : 显示新生代大小和使用情况
    jstat -gcold $pid 1000 : 显示老年代和永久代信息
    jstat -gcoldcapacity $pid 1000 : 显示老年代大小
    jstat -printcompilation $pid 1000 : 输出JIT编译的方法信息
    jstat -class $pid 1000 : 显示类加载器classloader相关信息
    jstat -compiler $pid 1000 : 显示JIT编译相关信息
    
  • 如果GC问题特别明显,通过jstat快速发现。我们可知启动命令行中加上参数-t (time),可以输出程序从启动到现在的时间。如果FGC和启动时间比值太大,说明系统吞吐量较小,GC花费时间太多。另外,如果老年代Full GC之后,没有内存占用明显下降,可能是内存达到瓶颈,或发生内存泄漏。如下面例子,追加GC时间增量和GC时间比率两列
    Jstat -gcutil -t 90542 1000 |awk ‘begin {pre=0} {if(NR>1) {print $0 “\t”  ($12-pre) “\t” $12*100/$1 ; pre=$12 } else {print $0 “\tGCT_INC\tRate”} }’
    Timestamp	S0	S1	E	O	M	CCS	YGC	YGCT	FGC	FGC	TGC	TGC_INC Rate 
    18.7	0.00	100.00	6.02	1.45	84.81	76.09	1	0.002	0	0.000	0.0020	0.010695219.7	0.00	100.00	6.02	1.45	84.81	76.09	1	0.002	0	0.000	0.0020	0.0101523 	````
    

GC日志也会搞鬼

  • 搞鬼例子,ES 插入、查询数据非常快。为压榨其性能,对磁盘读写几乎全速。它在后台对批次提交的数据做了很多Merge动作,将小块所有合并成大块索引。还有TransLog等预写动作,都是I/O大户
  • 解决思路
    1. 问题:其中一套ES机器,在访问高峰,多个ES节点发生严重STW问题,有点节点停顿了7~8s。用户态420ms,真实停顿7.62s [Times: user=0.42 sys=0.03, real=7.62 secs] 
    2.使用iostat -x 1 看到具体I/O使用情况
    3.盘点资源后,唯一超额利用就是I/O资源(%util保持在90以上),GC可能在等I/O4.通过搜索,发现有类似问题,
    原因:写GC日志的write动作,是统计在STW的时间中,我们场景中,由于ES索引数据,和GC放置一个磁盘,GC时写日志的动作,就和写数据文件发生资源争用。
    解决:将ES日志文件,单独放到一块普通HDD磁盘   
    

如何快速定位JVM相关GC问题_第12张图片

你可能感兴趣的:(jvm)