JVM系列之故障排查与性能调优(重点)

1、故障排查与性能调优

1.1、概述

1.1.1、生产环境中的问题?
  • 生产环境发生了OOM,该如何处理?如何判断是否是内存泄漏导致的?
  • 生产环境应该给Java进程分配多少内存?
  • 生产环境应该如何选择垃圾收集器?
  • 生产环境如何设置JVM参数?
  • 如何对垃圾收集器的性能进行调优?
  • 生产环境CPU负载飙高如何处理?
  • 生产环境线程池的参数如何设置?
  • 如何查看生产环境代码和本地代码是否一致
  • 不重启服务,修改代码加log,如何确定请求是否执行了某一行代码
  • 不重启服务,修改代码加log,如何实时查看某个方法的入参和返回值
1.1.2、为什么要进行调优
  • 防止出现OOM
  • 解决OOM
  • 减少Full GC出现的频率
  • 提高程序的吞吐量
  • 减少GC停顿时间
  • 减少Java进程的内存占用
1.1.3、调优概述
1.1.3.1、监控的依据
  • 运行日志
  • 堆栈异常信息
  • GC日志
  • 线程快照
  • 堆转储快照(内存快照)
1.1.3.2、调优的大方向
  • 合理编写代码
  • 充分并合理的使用硬件资源
  • 合理地进行JVM调优
1.1.3.3、性能优化的步骤
1.1.3.3.1、第一步(发现问题):性能监控
  • GC频繁
  • CPU load过高
  • 发生了OOM
  • 线程死锁
  • 内存泄漏
  • 程序响应时间较长
1.1.3.3.2、第二步(排查问题):性能分析
  • 打开GC日志,通过GC分析工具,分析GC日志
  • 灵活运用命令行工具:jstack、jmap、jinfo等
  • 使用阿里Arthas实时查看JVM状态
  • jstack查看堆栈信息
1.1.3.3.3、第三步(解决问题):性能调优
  • 适当增加内存,根据业务情况选择垃圾收集器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池参数
  • 使用中间件提高效率,比如缓存、消息队列等
1.1.3.4、性能评价
  • 停顿时间:在垃圾回收环节中,执行垃圾收集时,用户线程被暂停的时间,一般而言10ms就为优,有些注重低延迟的服务要求接口在50ms以内返回,接口执行的平均时间在40ms左右,那么就要求GC停顿时间不能超过10ms
  • 吞吐量:运行用户代码时间占Java进程总时间的比例(运行总时间:程序运行时间 + 内存回收的时间)
  • QPS:每秒响应请求数。例如1000个人同时在线,估计并发数在5% - 15%之间,也就是QPS在50 - 150之间
  • 内存占用:Java进程内存占用大小
  • 请求收到GC影响比例计算
    1. 受GC影响请求占比 = (接口响应平均时间(ms) + GC平均时间(ms)) * GC发生次数N / 程序运行时间T(ms)
    2. 该指标用于描述GC对接口的响应时间的影响,在注重程序可靠性时比较关心该指标,示例:例如在程序运行时间T内,发生N次GC,接口响应平均时间为50ms,GC平均时间为25毫秒,那么受GC影响请求占比 = (50 + 25) * N / T

JVM系列之故障排查与性能调优(重点)_第1张图片

示例:例如在程序运行时间T内,发生N次GC,接口响应平均时间为50ms,GC平均时间为25毫秒,那么受GC影响请求占比 = (50 + 25) * N / T

  1. 使用jstat -gc -t 命令,找到Timestamp、YGC、YGCT、FGC、FGCT列

    • Timestamp:JVM进程的运行时间,单位秒
    • YGC:YGC发生的次数
    • YGCT:YGC总共耗时,单位秒
    • FGC:FGC发生的次数
    • FGCT:FGC的总耗时,单位秒
  2. 受YGC影响请求占比 = (接口响应平均时间(ms) + YGCT / YGC * 1000) * YGC / (Timestamp * 1000)

  3. 受FGC影响请求占比 = (接口响应平均时间(ms) + FGCT / YGC * 1000) * FGC / (Timestamp * 1000)

1.2、JVM监控及诊断工具

1.2.1、命令行工具(重点)
1.2.1.1、jps(虚拟机进程状况工具)

JDK的很多小工具的名字都参考了UNIX命令的命名方式

  • jps(JVM Process Status Tool)是其中的典型,可以列出当前主机上正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier),该命令配合其他命令使用,主要是获取Java进程的id
  • 与ps -ef | grep java命令类似
  • 位置在JAVA_HOME的bin目录下
    JVM系列之故障排查与性能调优(重点)_第2张图片
1.2.1.2.1、基本语法

基本格式:jps [options]
JVM系列之故障排查与性能调优(重点)_第3张图片

options参数:

  • -q:只显示pid用户进程
  • -l:输出main类或Jar的全限名
  • -v:输出传入JVM的参数
  • -m:输出传入给main()方法的参数

hostid参数:查看远程主机上的Java进程,一般不使用。一般都是使用Xshell直接连接至远程服务器,实际使用jps -lmv命令就可以了

有些时候我们希望查看Java进程的某些JVM参数,或者实时修改某些JVM参数,那么jinfo命令可以帮我们做到这点

1.2.1.2、jinfo(实时查看和修改JVM配置参数)
1.2.1.2.1、基本情况

位置在JAVA_HOME/bin目录下
JVM系列之故障排查与性能调优(重点)_第4张图片

1.2.1.2.2、基本语法

基本语法:jinfo -option
JVM系列之故障排查与性能调优(重点)_第5张图片
查看

  • jinfo -sysprops PID,查看系统参数,Java代码通过System.getProperties(“参数名”)获取,通过JVM参数-Dkey=value,可以设置系统参数
  • jinfo -flags PID ,查看曾经赋值过的一些参数
  • jinfo -flag 具体参数名 PID ,查看对应参数的值

修改

  • 针对boolean类型,jinfo -flag +|- 具体参数名 PID
  • 针对非boolean类型,jinfo -flag 具体参数名=具体参数值 PID
  • 并不是所有参数都支持动态修改,查看支持动态修改的参数:java -XX:+PrintFlagsFinal -version | grep manageable,参数是manageable才能够进行动态修改
    JVM系列之故障排查与性能调优(重点)_第6张图片
1.2.1.2.3、拓展
  • java -XX:+PrintFlagsInital,查看所有JVM参数的启动初始值
  • java -XX:+PrintFlagsFinal,查看所有JVM参数的最终值
  • java -XX:+PrintCommandLineFlags,查看那些已经被用户或者JVM设置过的详细XX参数的名称和值
1.2.1.2.4、示例
  • 查看系统参数jinfo -sysprops PID
    JVM系列之故障排查与性能调优(重点)_第7张图片
  • 查看赋值的参数jinfo -flags PID
    JVM系列之故障排查与性能调优(重点)_第8张图片
  • 查看对应参数的值jinfo -flag 具体参数名 PID
    JVM系列之故障排查与性能调优(重点)_第9张图片
  • 针对boolean类型参数进行修改jinfo -flag +|- 具体参数名 PID
    JVM系列之故障排查与性能调优(重点)_第10张图片
  • 针对非boolean类型
    JVM系列之故障排查与性能调优(重点)_第11张图片
1.2.1.3、jstat(虚拟机统计信息监视工具)
1.2.1.3.1、基本情况

jstat是JDK自带的一个轻量级小工具。全称Java Virtual Machine statistics monitoring tool,jstat工具特别强大,有众多的可选项,详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程id和所选参数

在JAVA_HOME的bin目录下
JVM系列之故障排查与性能调优(重点)_第12张图片

1.2.1.3.2、基本语法

帮助信息
JVM系列之故障排查与性能调优(重点)_第13张图片
基本格式 jstat - [-t] [-h] [] []

option参数
JVM系列之故障排查与性能调优(重点)_第14张图片

  • class 显示classLoader相关信息

  • compiler 显示JIT编译相关信息

  • gc 显示与GC相关堆信息

  • gccapacity 显示各个代的容量以及使用情况

  • gccause 显示垃圾收集相关信息,最后一次垃圾回收的原因

  • gcnew 显示新生代信息

  • gcnewcapacity 显示新生代使用情况

  • gcold 显示老年代和永久的信息

  • gcoldcapacity 显示老年代的大小

  • gcpermcapactiy 显示永久代的大小

  • gcutil 显示垃圾收集信息

  • printcompilation 输出JIT编译的方法信息

  • interval 可选参数,指定输出统计数据的周期,单位毫秒,即为查询间隔

  • count 可选参数,指定查询的总次数

  • -t 可以在输出信息之前加上一个timestamp列,显示程序运行的时间,单位秒

  • -h 可以在周期性输出数据的同时,在输出多少行之后输出一个表头信息

1.2.1.3.3、示例
  • 示例1:jstat -gc pid 显示与GC相关的堆内存信息
    在这里插入图片描述

JVM系列之故障排查与性能调优(重点)_第15张图片

  • 这里可以计算一下吞吐量以及GC停顿时间

    1. 吞吐量:1 - (GC耗费的时间 / 程序运行的总时间),即1 - GCT / Timestamp
    2. GC停顿时间:单次GC所消耗的时间,单位毫秒,(YGCT + FGCT) / (YGC + FGC) * 1000,一般而言GC的停顿时间在200ms以内,响应时间算不错
  • 示例2:jstat -gccause pid 显示伊甸园区、幸存者区、老年代、元空间使用比例,以及上一次GC发生的原因

JVM系列之故障排查与性能调优(重点)_第16张图片
JVM系列之故障排查与性能调优(重点)_第17张图片

  • 示例3:jstat -gccapacity pid 显示堆内存各区域初始化大小、最大大小和占用大小
    在这里插入图片描述
    JVM系列之故障排查与性能调优(重点)_第18张图片
1.2.1.4、jmap(导出内存映像文件&内存使用情况)
1.2.1.4.1、基本情况

jmap全称(JVM Memory Map):作用一方面获取dump文件(堆转储快照,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆内存各区域的使用情况、堆中对象的统计信息、类加载信息等

位置在JAVA_HOME/bin目录下
JVM系列之故障排查与性能调优(重点)_第19张图片

1.2.1.4.2、基本语法

JVM系列之故障排查与性能调优(重点)_第20张图片
基本格式:jmap [option] pid
option参数

  • -dump:生成Java堆转储快照:dump文件,特别地:-dump:live只保存堆中存活的对象
  • -heap:输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
  • -histo:输出堆中对象的统计信息,包括类、实例数量和合计容量,特别的:-history:live只统计堆中存活的对象
  • -finalizerinfo:显示在F-queue中等待Finalizer线程执行finalize方法的对象
  • -F:当虚拟机进程对-dump没有任何响应时,可以使用此参数强制生成dump文件
1.2.1.4.3、示例
  • 示例1:导出堆转储快照文件

    1. 手动的方式,jmap -dump:live,format=b,file=
      JVM系列之故障排查与性能调优(重点)_第21张图片
      dump指定Java进程的内存快照,只保存存活对象,并且保存到指定路径下
    2. 自动的方式(添加JVM参数)
      1. -XX:+HeapDumpOnOutOfMemoryError
      2. -XX:HeapDumpPath=

    显示Java进程堆内存的配置信息和使用情况
    JVM系列之故障排查与性能调优(重点)_第22张图片

  • 示例2:堆内存使用情况(jmap -heap )
    JVM系列之故障排查与性能调优(重点)_第23张图片

  • 示例3:查看存活对象情况(jmap -histo:live )
    JVM系列之故障排查与性能调优(重点)_第24张图片
    JVM系列之故障排查与性能调优(重点)_第25张图片
    可以通过该命令可以判断对象生成的速率,例如前后间隔5秒,打印出对象直方图。查看所属类实例的增长情况

jmap相关的命令不能在生产环境随便使用,尤其是dump内存快照这个命令,当想要获取JVM运行时的内存快照时,需要满足让所有的用户线程都停止在安全点或者是安全区,停止所有的用户线程(STW)这样对象间的引用关系不会发生变化,得到的dump结果才是正确的

1.2.1.5、jstack(Java堆栈跟踪工具)
1.2.1.5.1、基本情况

jstack命令可以dump当前运行JVM的线程快照,线程快照是当前Java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。

在JAVA_HOME/bin目录下
JVM系列之故障排查与性能调优(重点)_第26张图片

1.2.1.5.2、基本语法

JVM系列之故障排查与性能调优(重点)_第27张图片
基本格式 jstack -
option参数

  • -F 当正常输出的请求不被响应时,强制输出线程堆栈
  • -l 除了堆栈外,显示关于锁的附加信息
  • -m 如果调用了本地方法,显示C/C++的堆栈
1.2.1.5.3、示例

示例:dump某个Java进程的线程快照,并且输出锁的附加信息
Java进程中包含JVM线程和用户线程,

JVM线程:

  • JIT编译线程:如C1 CompilerThread0、C2 CompilerThread0
  • GC线程: 如GC Thread0、G1 Young RemSet Sampling
  • 其它内部线程: 如VM Periodic Task Thread、VM Thread、Service Thread
  • Finalizer线程:用于执行对象重写Object类的finalize()方法的线程
  • Referencer线程:用于执行在引用队列中的事件
  • Attach Listener:接收外部命令(jstat、jinfo、jmap等)的线程
  • Signal Dispatcher:当Attach Listener线程接收到命令后,会交给Signal Dispatcher线程去进行分发到各个不同的模块处理命令,并且返回处理结果
  • DestroyJavaVM:JVM在服务器启动之后,就会唤起DestroyJavaVM线程,处于等待状态,等待其它线程(Java线程和native线程)退出时通知它卸载JVM。线程退出时,都会判断自己当前是否是整个JVM中最后一个非daemon线程,如果是,则通知DestroyJavaVM线程卸载JVM

打印内容:

  • 线程名称
  • 线程优先级
  • 线程16进制id
  • 线程的状态
  • 线程等待的锁对象
  • 线程的方法调用栈

JVM系列之故障排查与性能调优(重点)_第28张图片

1.2.1.6、arthas(Java应用诊断利器)
1.2.1.6.1、基本概述

Arthas是Alibaba开源的Java诊断工具,深受开发者喜爱, Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。

官方文档:

主要应用的场景:

  • 这个类从哪个jar包加载的?为什么会报各种类相关的Exception?
  • 我改的代码为什么没有执行到?难道是我没commit?分支搞错了?
  • 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?
1.2.1.6.2、安装、卸载和启动
  • 安装
    1. 下载arthas-boot.jar,通过curl -O https://arthas.aliyun.com/arthas-boot.jar命令下载或者下载速度比较慢,可以使用aliyun的镜像:java -jar arthas-boot.jar --repo-mirror aliyun --use-http
    2. 使用as.sh,Arthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲回车执行即可:curl -L https://arthas.aliyun.com/install.sh | sh,上述命令会下载启动脚本文件as.sh到当前目录
    3. 在releases页面下载rpm/deb包: https://github.com/alibaba/arthas/releases,然后安装deb sudo dpkg -i arthas*.deb,再接着安装rpm sudo rpm -i arthas*.rpm,最后在安装后,可以直接执行:as.sh。
  • 卸载
    1. 在Linux/Unix/Mac平台删除下面文件:rm -rf ~/.arthas/ rm -rf ~/logs/arthas
    2. Windows平台直接删除user home下面的.arthas和logs/arthas目录
  • 启动
    1. jar包的方式:java -jar arthas-boot.jar
    2. 脚本的方式:sh as.sh或者是./as.sh
  • 查看日志:cat ~/logs/arthas/arthas.log
  • 查看帮助:java -jar arthas-boot.jar -h
  • 退出:如果只是退出当前的连接,可以用quit或者exit命令。Attach到目标进程上的arthas还会继续运行,端口会保持开放,下次连接时可以直接连接上,如果想完全退出arthas,可以执行stop命令
1.2.1.6.3、诊断命令
  • 基础指令
    1. help——查看命令帮助信息
      JVM系列之故障排查与性能调优(重点)_第29张图片
      2. cat——打印文件内容,和linux里的cat命令类似
      3. echo–打印参数,和linux里的echo命令类似
      4. grep——匹配查找,和linux里的grep命令类似
      5. pwd——返回当前的工作目录,和linux命令类似
      6. cls——清空当前屏幕区域
      7. history——打印命令历史
      8. quit——退出当前Arthas客户端,其他Arthas客户端不受影响
      9. stop——关闭Arthas服务端,所有Arthas客户端全部退出
  • JVM相关
    1. dashboard——当前系统的实时数据面板
    JVM系列之故障排查与性能调优(重点)_第30张图片
    JVM系列之故障排查与性能调优(重点)_第31张图片
    2. thread——查看当前JVM的线程堆栈信息
    JVM系列之故障排查与性能调优(重点)_第32张图片
    示例1:thread -n 3,一键展示当前最忙的前N个线程并打印堆栈,如果后台GC线程经常性非常繁忙,说明GC频繁,这时需要观察应用的运行状况了,可能是堆内存不够了,即将要发生OOM了
    JVM系列之故障排查与性能调优(重点)_第33张图片
    示例2:thread -b, 找出当前阻塞其他线程的线程
    在这里插入图片描述
    示例3:thread --state ,查看指定状态的线程
    在这里插入图片描述
    3. heapdump——dump java heap,类似jmap命令的heap dump功能,和jmap命令类似,dump当前JVM进程的内存快照,-l或者是–live参数,只dump存活的对象,示例:只dump存活的对象到指定文件
    在这里插入图片描述
    4. logger——查看和修改logger,可以热更新某个类的日志级别,对于生产环境排查错误非常有用,例如当生产环境发生异常,线上无法debug,线下无法复现。可以动态修改类的日志级别为debug,打印更多的日志信息帮助排查,当然了前提是输出的debug日志对你有帮助,不然就没有意义了,示例:动态修改日志级别从info为debug,排查完成之后改回成info级别。

HelloController的代码,访问http://localhost:8080/test,控制台打印不同级别的日志信息
JVM系列之故障排查与性能调优(重点)_第34张图片
logback-spring.xml的配置文件,默认为info级别
JVM系列之故障排查与性能调优(重点)_第35张图片
控制台输出info级别及以上的日志信息
在这里插入图片描述
接下动态修改HelloControlller的日志级别为debug,并进行访问测试,使用sc -d 全类名命令,查看加载该类的类加载器的hashcode值
JVM系列之故障排查与性能调优(重点)_第36张图片
使用logger -n 全类名命令,查看待修改类的日志级别
JVM系列之故障排查与性能调优(重点)_第37张图片
使用logger -c 类加载器的hash值 -n 全类名 -l debug命令,修改日志级别
在这里插入图片描述
使用curl命令进行访问测试
在这里插入图片描述
控制台输出debug及以上级别的日志
在这里插入图片描述
使用logger -c 类加载器的hash值 -n 全类名 -l info命令重新修改为info级别
在这里插入图片描述

  1. jvm——查看当前JVM的信息,可以显示当前JVM的运行时信息、内存信息和加载的类信息等
    JVM系列之故障排查与性能调优(重点)_第38张图片

  2. sysprop——查看和修改JVM的系统属性

    JVM系列之故障排查与性能调优(重点)_第39张图片
    jinfo -sysprops 命令类似,查看JVM系统属性,
    在这里插入图片描述
    修改JVM某个系统属性,将user.country从CN修改为US,修改完成之后查看
    JVM系列之故障排查与性能调优(重点)_第40张图片

  3. sysenv——查看JVM的环境变量,查看当前JVM的环境属性(System Environment Variables)
    在这里插入图片描述

  4. vmoption——查看和修改JVM里诊断相关的option
    JVM系列之故障排查与性能调优(重点)_第41张图片
    vmoption基本上都是可以通过jinfo查看以及动态修改的JVM参数,查看所有参数:vmoption
    JVM系列之故障排查与性能调优(重点)_第42张图片
    查看指定参数的值:vmoption PrintGCDetails
    在这里插入图片描述
    修改指定参数的值:vmoption HeapDumpOnOutOfMemoryError true
    在这里插入图片描述

  • class、classloader相关
    1. jad——反编译指定已加载类的源码,这个指令非常有用,可以将类进行反编译,jad命令将类反编译后就可以查看源码,与本地代码进行比对
      JVM系列之故障排查与性能调优(重点)_第43张图片
      反编译java.lang.String类的toString()方法,并且重定向到String.java中,命令jad --source-only java.lang.String
      在这里插入图片描述

    2. sc——查看JVM已加载的类信息
      sc是search class的简写,顾名思义查看类的详细信息,该命令主要是查看加载该类的类加载器的hash值
      JVM系列之故障排查与性能调优(重点)_第44张图片
      查看java.lang.String的信息
      JVM系列之故障排查与性能调优(重点)_第45张图片

    3. mc——内存编译器,内存编译.java文件为.class文件
      编译生成.class文件之后,可以结合retransform命令实现热更新代码,注意,mc命令有可能失败。如果编译失败可以在本地编译好.class文件,再上传到服务器
      JVM系列之故障排查与性能调优(重点)_第46张图片
      先使用jad命令进行反编译,然后使用vim命令,修改源码,添加了一条测试输出
      在这里插入图片描述
      使用mc -c 类加载器的hash值 源码文件名命令,进行内存编译
      JVM系列之故障排查与性能调优(重点)_第47张图片

    4. retransform——加载外部的.class文件,retransform到JVM里
      加载指定的.class 文件,然后解析出class name,再使用retransform命令替换JVM中已加载的对应的类,每加载一个.class 文件,则会记录一个retransform entry,不同的是这个只是临时替换,还有机会可以反悔,也就是删除retransform entry即可

示例:修改HelloController中的代码进行测试
在这里插入图片描述
使用curl命令进行测试
在这里插入图片描述
消除retransform的影响,只需要删除对应的entry即可
在这里插入图片描述
重新使用curl命令进行测试,发现测试日志没有了
在这里插入图片描述
retransform的限制:不允许新增、删除field、method,不允许修改field名,不允许修改方法参数、方法名称及返回值,只能修改方法体中的内容,正在跑的函数,没有退出不能生效

上传.class文件到服务器的技巧:有的服务器不允许直接上传文件,可以使用base64命令来绕过,在本地先转换.class文件为base64,再保存为result.txt,即base64 < Test.class > result.txt,到服务器上,新建并编辑result.txt,复制本地的内容,粘贴再保存,把服务器上的result.txt还原为.class,base64 -d < result.txt > Test.class

  1. redefine——加载外部的.class文件,redefine到JVM里
    可以实现代码的热更新,redefine的class不能修改、添加、删除类的field和method,包括方法参数、方法名称及返回值,和retransform命令类似,区别在于redefine后的原来的类不能恢复,retransform可以通过删除entry恢复

  2. sm——查看已加载类的方法信息
    Search-Method的简写,这个命令能搜索出所有已经加载了Class信息的方法信息,查看java.lang.String的toString方法
    JVM系列之故障排查与性能调优(重点)_第48张图片

  3. classloader——查看classloader的继承树、urls、类加载信息
    JVM系列之故障排查与性能调优(重点)_第49张图片
    按类加载实例查看统计信息
    在这里插入图片描述
    可以查看某个类加载器实际的urls,这里的urls指类加载器加载的路径,也就是说会把该路径下的所有类加载到内存中,可以让指定的classloader去getResources,打印出所有查找到的resources的url。对于ResourceNotFoundException比较有用,查看URLClassLoader实际的urls
    JVM系列之故障排查与性能调优(重点)_第50张图片
    使用ClassLoader去查找resource
    在这里插入图片描述
    使用ClassLoader去查找class文件
    在这里插入图片描述

  • watch
  1. 观察指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写OGNL表达式进行对应变量的查看

  2. 命令格式watch [-option] class-pattern method-pattern [express] [condition-express]

  3. 命令解释:观察类的某个方法的执行情况,包括入参、返回值(express表达式:“{params,returnObj}”)等。有时候观察所有情况无意义,可以在特定条件下触发,例如运行时间大于100ms(condition-express:‘#cost>200’)
    JVM系列之故障排查与性能调优(重点)_第51张图片
    express:观察表达式,观察表达式的构成主要由ognl表达式组成,可以写"{params,returnObj}",只要是一个合法的ognl表达式,都能被正常支持
    JVM系列之故障排查与性能调优(重点)_第52张图片
    condition-express:条件表达式,当条件表达式为true时,才进行输出,例如发生异常才输出(-e选项),例如当耗时大于200ms时才会输出,过滤掉执行时间小于200ms的调用(#cost>200(单位是ms)),例如第一个参数小于0时才输出(“params[0]<0”)

  4. 特别说明,watch命令定义了4个观察事件点,即-b(before)方法调用前,-e(exception)方法异常后,-s(success)方法返回后,-f(e + s)方法结束后,4个观察事件点-b、-e、-s默认关闭,-f默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出,这里要注意方法入参和方法出参的区别,有可能在中间被修改导致前后不一致,除了-b事件点params代表方法入参外,其余事件都代表方法出参,当使用-b时,由于观察事件点是在方法调用前,此时返回值或异常均不存在

  5. 案例:

    1. 示例1:观察方法出参和返回值
      JVM系列之故障排查与性能调优(重点)_第53张图片
    2. 示例2:观察方法入参,对比前一个例子,返回值为空(事件点为方法执行前,因此获取不到返回值)
      JVM系列之故障排查与性能调优(重点)_第54张图片
    3. 示例3:同时观察方法调用前和方法返回后,参数里-n 2,表示只执行两次,这里输出结果中,第一次输出的是方法调用前的观察表达式的结果,第二次输出的是方法返回后的表达式的结果,结果的输出顺序和事件发生的先后顺序一致,和命令中-s -b的顺序无关
      JVM系列之故障排查与性能调优(重点)_第55张图片
    4. 示例4:条件表达式的例子,只有满足条件的调用,才会有响应JVM系列之故障排查与性能调优(重点)_第56张图片
    5. 示例5:观察异常信息的例子,-e表示抛出异常时才触发,express中,表示异常信息的变量是throwExp
      JVM系列之故障排查与性能调优(重点)_第57张图片
    6. 示例6:按照耗时进行过滤,#cost>200 (单位是ms)表示只有当耗时大于0.02ms时才会输出,过滤掉执行时间小于0.02ms的调用
      JVM系列之故障排查与性能调优(重点)_第58张图片
  • trace
    trace命令能主动搜索class-pattern/method-pattern对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路,命令格式trace [-options] class-pattern method-pattern [condition-express],命令解释:在满足条件表达式(condition-express)的情况下,根据options选项输出某个类下的某个方法的调用节点耗时
    JVM系列之故障排查与性能调优(重点)_第59张图片
    1. 案例1:打印方法执行时间超过0.1ms的,节点耗时
      JVM系列之故障排查与性能调优(重点)_第60张图片

    2. 案例2:打印包含JDK的函数耗时,–skipJDKMethod skip jdk method trace, default value true,默认情况下,trace不会包含JDK里的函数调用,如果希望trace JDK里的函数,需要显式设置–skipJDKMethod false
      演示案例:打印包含JDK的函数耗时
      JVM系列之故障排查与性能调优(重点)_第61张图片

  • monitor
    1. 对匹配class-pattern/method-pattern/condition-express的类、方法的调用进行监控,可以统计某个方法在一段时间的执行时间和结果,对于排查系统性能瓶颈很有帮助,monitor命令是一个非实时返回命令。实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标Java进程返回信息,直到用户输入Ctrl+C为止

    2. 命令格式monitor [-option] class-pattern method-pattern [condition-express]
      JVM系列之故障排查与性能调优(重点)_第62张图片

    3. 示例1:计算条件表达式过滤统计结果(方法执行完毕之后)
      在这里插入图片描述

    4. 示例2:计算条件表达式过滤统计结果(方法执行完毕之前)
      在这里插入图片描述

  • stack
    1. 很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是stack命令
    2. 命令格式stack [-options] class-pattern [method-pattern] [condition-express]
    3. 案例1:根据条件表达式来过滤 JVM系列之故障排查与性能调优(重点)_第63张图片
    4. 案例2:根据执行时间来过滤
      JVM系列之故障排查与性能调优(重点)_第64张图片
  • tt、profiler
    1. tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
    2. 缘由,watch虽然很方便和灵活,但需要提前想清楚观察表达式的编写(condition-express编写),这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测,这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助,于是乎,TimeTunnel命令就诞生了
    3. 命令格式tt [-options] [class-pattern] [method-pattern] [condition-express]
    4. options参数
      1. -t参数,必带参数:tt 命令有很多个主参数,-t 就是其中之一。这个参数的表明希望记录下类*Test的print方法的每次执行情况
      2. -n 3,当你执行一个调用量不高的方法时可能你还能有足够的时间用CTRL+C中断tt命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的JVM内存撑爆,此时你可以通过-n 参数,指定你需要记录的次数,当达到记录次数时Arthas会主动中断tt命令的记录过程,避免人工操作无法停止的情况
      3. -l,展示所有的方法调用的记录
      4. -s ,一般和-l一起使用,和condition-express类似,进行过滤
      5. -i ,index的简写,显示某个具体调用的方法,value的值可以从-l打印的index值获得
    5. 解决方法重载和指定参数
      1. 方法参数个数不一致,tt -t *Test print params.length==1
      2. 方法参数数据类型不一致,tt -t *Test print ‘params[1] instanceof Integer’
      3. 解决特定参数,tt -t *Test print params[0].mobile==“13989838402”
    6. 案例演示
      1. 记录调用,对于一个最基本的使用来说,就是记录下当前方法的每次调用环境现场,记录10次primeFactors方法的调用
      JVM系列之故障排查与性能调优(重点)_第65张图片
      JVM系列之故障排查与性能调优(重点)_第66张图片
      2. 检索调用记录,tt -l,查看所有的调用记录
      JVM系列之故障排查与性能调优(重点)_第67张图片
      tt -s <搜索表达式,ognl表达式>,案例tt -s ‘isReturn==false’,只查看失败的调用
      JVM系列之故障排查与性能调优(重点)_第68张图片
      3. 查看调用信息
      对于具体一个时间片的信息而言,你可以通过-i参数后边跟着对应的INDEX编号查看到他的详细信息,tt -i 1024,查看index为1024的方法调用过程
      JVM系列之故障排查与性能调优(重点)_第69张图片
      4. 重做一次调用
      tt命令由于保存了当时调用的所有现场信息,所以我们可以自己主动对一个INDEX编号的时间片自主发起一次调用,-p参数进行重新调用,–replay-times 指定调用次数,–replay-interval 指定多次调用间隔(单位ms, 默认1000ms)

tt -i 1024 -p --replay-times 1
JVM系列之故障排查与性能调优(重点)_第70张图片
你会发现结果虽然一样,但调用的路径发生了变化,由原来的程序发起变成了Arthas自己的内部线程发起的调用了。

  1. 观察表达式
    表达式核心变量所有变量都可以打印,如成员变量、方法输入参数、返回参数等。

案例1:打印静态成员变量
在这里插入图片描述

案例2:调用静态成员的方法
在这里插入图片描述

  1. 删除调用记录

tt --delete-all,删除所有的方法调用记录

1.2.2、GUI可视化工具

1.3、JVM运行时参数

1.3.1、JVM参数选项类型
1.3.1.1、类型一:标准参数选项

以-开头,比较稳定,后续版本基本不会发生变化

例如-D<名称>=<值>,设置环境变量的值,Java代码可以通过System.getenv(名称)方法获取设置的值
JVM系列之故障排查与性能调优(重点)_第71张图片
运行java或者java -help可以看到所有的标准选项

1.3.1.2、类型二:-X参数选项

非标准化参数,功能比较稳定,后续版本可能发生变化,以-X开头

运行java -X命令可以看到所有的X选项
JVM系列之故障排查与性能调优(重点)_第72张图片
JVM的JIT编译模式相关的选项

  • -Xint,禁用JIT,所有的字节码都被解释执行,这个模式是最慢的
  • -Xcomp,所有字节码第一次使用就都被编译成本地机器码,然后执行
  • -Xmixed,混合模式,默认的模式,根据程序的运行情况,JIT编译线程选择性地将某些热点代码编译成本地机器码

注意:

  • -Xms,设置初始Java堆大小,等价于-XX:InitialHeapSize
  • -Xmx,设置最大Java堆大小,等价于-XX:MaxHeapSize
  • -Xss,设置java线程栈大小,等价于-XX:ThreadStackSize
1.3.1.3、类型三:-XX参数选项

非标准参数,使用最多的参数类型,这类选项属于实验性,不稳定,以-XX开头,主要作用是用于开发和调试JVM

1.3.1.3.1、分类
  • Boolean类型格式
    • -XX:+表示启用option属性
    • -XX:-表示禁用option属性
    • 例如:-XX:+PrintGCDetails,打印GC详细日志
  • 非Boolean类型格式(key-value类型)
    • 数值类型格式,-XX:=,修改Eden区和Survivor区比例大小-XX:SurvivorRatio=8
    • 非数值类型格式,-XX:=,设置内存快照dump文件的路径-XX:HeapDumpPath=./dump,需要注意的是,如果填写的是目录,目录必须存在

-XX:+PrintFlagsFinal可以输出所有参数的名称和默认值

1.3.2、添加JVM参数选项

JVM系列之故障排查与性能调优(重点)_第73张图片

  • 运行jar包,命令如下java -Xmx50m -Xms50m -XX:+PrintGCDetails -jar Demo.jar [args]
  • 通过Tomcat运行war包
    1. 在Linux系统下,tomcat_home/bin/catalina.sh中添加类似如下配置:JAVA_OPTIONS=-Xms512m -Xmx1024m
    2. 在window系统下,catalina.bat中添加如下配置 set “JAVA_OPTS=-Xms521m”
  • 程序运行过程中
    1. 使用jinfo -flag = 命令,设置非Boolean类型参数
    2. 使用jinfo -falg [+|-] 命令,设置Boolean类型参数
    3. 使用arthas的vmoption命令修改
    4. 当然这些参数都只能是在运行期间能够动态修改的参数
    5. 可以使用java -XX:+PrintFlagsFinal -version | grep manageable命令进行查看哪些参数支持动态修改
1.3.3、常用JVM参数选项
1.3.3.1、打印设置的XX选项及值
  • -XX:+PrintCommandLineFlags,打印程序运行前用户手动设置,或者JVM自动设置的参数
  • -XX:+PringFlagsInitial,打印所有XX选项的默认值
  • -XX:+PrintFlagsFinal,打印XX选项在程序运行时生效的值
  • -XX:+PrintVMOptions,打印JVM的参数
1.3.3.2、堆、栈、方法区等内存大小设置
1.3.3.2.1、栈

-Xss128k,设置每个线程的栈大小为128k,等价于-XX:ThreadStackSize=128k

1.3.3.2.2、堆内存
  • -Xms3550m,等价于-XX:InitialHeapSize,设置JVM初始堆内存为3550m
  • -Xmx3550m,等价于-XX:MaxHeapSize,设置JVM最大堆内存为3550m
  • -Xmn2g,设置新生代大小为2g,官方推荐配置为整个堆大小的3 / 8
  • -XX:NewSize=1024m,设置新生代的初始值为1024m
  • -XX:MaxNewSize=1024m,设置新生代最大值为1024m
  • -XX:NewRatio=4,设置老年代和新生代(Eden伊甸园区和两个Survivor幸存者区)的比值,默认值为2
  • -XX:SurvivorRatio=8,设置Eden和Survivor的比例大小为8,默认为8
  • -XX:+UseAdaptiveSziePolicy,自动选择各区大小比例
  • -XX:MaxTenuringThreshold,每经历过一次MinorGC后,仍然存活的对象,年龄+1,对象年龄晋升的阈值,当大于该值时,晋升到老年代,默认值为15
  • -XX:PretenureSizeThreadshold=1024,设置让大于此阈值的对象直接分配在老年代,单位为字节,只对Serial、PerNew垃圾收集器有效
  • -XX:+PrintTenuringDistribution,让JVM在每次MinorGC后打印当前使用的Survivor中对象的年龄分布
  • -XX:TargetSurvivorRatio,表示MinorGC结束后Survivor区域中占用空间的期望比例
1.3.3.2.3、方法区
  • 永久代
    1. -XX:PermSize=256m,设置永久代初始值为25m
    2. -XX:MaxPermSize=256m,设置永久代最大值为256m
  • 元空间
    1. -XX:MetaspaceSize,设置初始元空间大小
    2. -XX:MaxMetaspaceSize,设置最大元空间大小
    3. -XX:+UseCompressOops,启用压缩对象指针
    4. -XX:CompressClassSpaceSize,设置Klass Metaspace的大小,默认1G
1.3.3.2.4、直接内存

-XX:MaxDirectMemorySize,设置直接内存大小,未指定默认和Java堆内存的最大值一样

1.3.3.3、OutOfMemory相关设置
  • -XX:+HeapDumpOnOutOfMemoryError,表示出现OOM的时候,dump内存快照
  • -XX:+HeapDumpBeforeFullGC,表示出现FullGC之前,生成dump文件
  • -XX:+HeapDumpAfterFullGC,表示出现FullGC后,生成dump文件
  • -XX:HeapDumpPath= ,指定dump文件路径
  • -XX:OnOutOfMemoryError,指定一个脚本,当发生OOM的时候,执行这个脚本
    1. 这个选项非常有用,一般而言,生成发生OOM是很严重的错误,需要及时通知到运维人员
    2. 可以在脚本中设置发短信、钉钉、邮件等方式通知运维人员
1.3.3.4、垃圾收集器相关设置
1.3.3.4.1、垃圾收集器组合关系
  • 新生代和老年代垃圾收集器图示
    JVM系列之故障排查与性能调优(重点)_第74张图片
  • 新生代和老年代垃圾收集器组合关系以及后续变化
    JVM系列之故障排查与性能调优(重点)_第75张图片
  • 红线组合在JDK8中标记为过期
  • 红线组合在JDK9中进行了移除
  • 绿线组合在JDK14中进行了移除
  • 在JDK9中标记CMS为过期的,在JDK14中删除了CMS垃圾收集器
  • 在JDK8中默认的组合是Parallel Scavenge GC和Parallel Old GC
  • 在JDK9中默认的组合是G1 GC
  • 在Client模式下默认的组合是Serial GC和Serial Old GC
1.3.3.4.2、查看默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

在这里插入图片描述
JAVA8默认使用Parallel并行垃圾收集器,使用jinfo -flag 垃圾回收器选项命令,查看是否启用对应的垃圾收集器

1.3.3.4.3、Serial收集器

Serial收集器作为HotSpot中Client模式下默认的新生代垃圾收集器,
-XX:+UseSerialGC,启用Serial收集器,开启后,新生代和老年代默认都启用串行也就是Serial和Serial Old收集器

1.3.3.4.4、ParNew收集器
  • -XX:+UseParNew,启用ParNew收集器,手动指定年轻代使用ParNew垃圾收集器,主要是配合CMS垃圾收集器使用,老年代默认Serial Old垃圾收集器,可以手动指定使用CMS垃圾收集器
  • -XX:ParallelGCThreads=N,指定并行线程数量,默认值为CPU核心数
1.3.3.4.5、Parallel收集器
  • -XX:+UseParallelGC,手动指定年轻代启用Parallel并行垃圾收集器
  • -XX:+UseParallelOldGC,手动指定老年代启用Parallel Old并行垃圾收集器,JDK8默认启用这两个垃圾收集器,一个被激活,另一个也会默认开始(互相激活)
  • -XX:ParallelGCThreads,设置年轻代并行收集器的线程数,一般的,默认与CPU核心数相等,以避免过多的线程数量影响垃圾收集器性能,默认情况下,当CPU数量小于8个时,ParallelGCThreads的值等于CPU数量,当CPU数量大于8个时,ParallelGCThreads的值等于3 + 5 * CPU_Count / 8
  • -XX:MaxGCPauseMillis,设置垃圾收集器最大停顿时间(即STW时间),单位是毫秒,为了尽可能把停顿时间控制在MaxGCPauseMillis以内,收集器会在工作时调整JAVA堆大小或者其他参数
  • -XX:GCTimeRatio=N,用户线程运行时间和垃圾回收线程运行时间比例,取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%,垃圾收集器时间占总时间的比例(1 / (N + 1)),与前一个参数-XX:MaxGCPauseMillis参数存在一定矛盾性
  • -XX:+UseAdaptiveSizePolicy,设置Parallel Scavenge收集器具有自适应调节策略,在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数都会被自动调整,已达到堆大小、吞吐量和停顿时间之间的平衡点,在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量和停顿时间,让虚拟机自己完成调优工作
1.3.3.4.6、CMS收集器
  • -XX:+UseConcMarkSweepGC,老年代手动启用CMS垃圾收集器,开启该参数后-XX:+UseParNewGC参数自动打开。即使用ParNew(Young)+ CMS(Old)+ Serial Old(后备方案)的组合
  • -XX:CMSInitiatingOccupancyFraction,设置堆内存使用率的阈值,一旦达到该阈值,便开始进行内存回收,JDK5及以前默认值为68,JDK6及以后默认值为92,如果内存增长缓慢,可以设置一个较大的值,该参数可以有效降低Full GC的执行次数
  • -XX:+UseCMSCompactAtFullCollection,指定执行完Full GC后,对内存空间进行压缩整理,以避免内存碎片的产生,不过由于压缩整理过程无法并发执行,停顿时间也就更长了
  • -XX:CMSFullGCsBeforeCompaction,在执行多少次Full GC后对内存进行压缩整理,默认值为0,表示每次进入Full GC时都进行碎片整理
  • -XX:ParallelCMSThreads,设置CMS的线程数量,CMS默认启动的线程数是(ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行收集器的线程数
  • 补充参数
    1. -XX:ConcGCThreads,设置并发垃圾收集的线程数
    2. -XX:+UseCMSInitiatingOccupancyOnly,是否动态可调节,用这个参数可以使CMS一直按CMSInitiatingOccupancyFraction设定的值启动
    3. -XX:+CSMScavengeBeforeRemark,强制HotSpot虚拟机在CMS的remark阶段之前做一次Minor GC,用于提高remark阶段的速度
    4. -XX:+CMSClassUnloadingEnable,如果有的话,启动回收Perm区(JDK8之前)
    5. -XX:+CMSParallelInitialEnabled,用于开始CMS的initial-mark阶段采用多线程的方式进行初始标记,在JAVA8默认开启
    6. -XX:+CMSParallelRemarkEnabled,用于开启CMS的remark阶段采用多线程的方式进行重新标记,默认开启
    7. -XX:+ExplicitGCInvokesConcurrent、-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses,这两个参数用户指定HotSpot虚拟机在执行System.gc()时使用CMS收集器
    8. -XX:+CMSPrecleaningEnabled,指定CMS是否需要进行Pre cleaning这个阶段
  • 在JDK9中,CMS被标记为过期的,如果在JDK9以上版本的HotSpot虚拟机使用-XX:+UseConcMarkSweepGC来开启使用CMS,用户会收到一个警告信息,提示未来会被废弃
  • 在JDK14中,删除CMS垃圾收集器,移除了CMS垃圾收集器,如果在JDK14中使用-XX:+UseConcMarkSweepGC,JVM不会报错,只是给出一个Warning信息,但是不会exit,JVM会自动使用默认的GC方式启动JVM
1.3.3.4.7、G1收集器
  • -XX:+UseG1GC,启用G1垃圾收集器,G1垃圾收集器既可以工作在老年代也可以工作在新生代
  • -XX:MaxGCPauseMillis,设置期望达到的最大GC停顿时间,单位是毫秒。默认值是200ms
  • -XX:ParallelGCThreads,设置STW时GC线程的数量,最多设置为8
  • -XX:ConcGCThreads=N,设置并发标记的线程数,将N设置为ParallelGCThreads的1 / 4左右
  • -XX:InitiatingHeapOccupancyPercent,设置触发GC周期的堆内存占用比例阈值,超过该值就会触发GC,默认值是45
  • -XX:G1NewSizePercent、-XX:G1MaxSizePercent,整个新生代占整个堆内存的最小百分比(默认5%)、最大百分比(默认60%)
  • -XX:G1ReservePercent=10,保留内存区域,防止to space(Survivor中的to区)溢出
  • Mixed GC调优参数
    1. 注意G1收集器也有Mixed GC,Mixed GC会回收Young区和部分的Old区
    2. -XX:InitiatingHeapOccupancyPercent,设置堆内存的使用比例,达到这个数值会触发global concurrent marking(全局并发标记),默认为45%,值为0表示间断进行全局并发标记
    3. -XX:G1MixedGCLiveThresholdPercent,设置Old区region被回收时候的对象占比,默认85%,只有Old区region中存活的对象占用达到了这个百分比才会在Mixed GC中被回收
    4. -XX:G1HeapWastePercent,在global concurrent marking(全局并发标记)结束后,可以知道所有的region有多少空间要被回收,在每次Young GC之后和再次发生Mixed GC之前,会检查垃圾占比是否到达此参数,只有达到了下次才会发生Mixed GC
    5. -XX:G1MixedGCCountTarget,在global concurrent marking(全局并发标记)之后,最多执行Mixed GC的次数,默认是8
    6. -XX:G1OldCSetRegionThresholdPercent,设置Mixed GC收集周期中要收集的Old region数的上限,默认值是Java堆的10%
1.3.3.4.8、如何选择垃圾收集器
  • 生产环境的JDK一般都是8及其以上
  • 优先使用默认的垃圾收集器也就是Parallel Scavenge + Parallel Old组合
  • 优先调整堆的大小让JVM自适应完成
  • 如果内存小于100M,使用Serial收集器
  • 如果是单核、单机程序,并且没有停顿时间要求,使用Serial收集器
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者让JVM自行选择
  • 如果是多CPU、追求低延迟,需要快速响应,使用并发收集器。官方推荐G1,性能高

没有最好的收集器,更没有万能的收集器,调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

1.3.3.5、GC日志相关参数
1.3.3.5.1、常用参数
  • -verbose:gc,输出GC日志信息,默认输出到标准输出中,可以独立使用

  • -XX:+PrintGC,等同于-verbose:gc,可以独立使用

  • -XX:+PrintGCDetails,在发生垃圾回收时打印内存回收详细信息,并且在进程退出时输出内存各区域的使用情况,可以独立使用

  • -XX:+PrintGCTimeStamps,输出GC发生的时间戳。

    1. 到发生GC时,程序应该运行了多少秒
      在这里插入图片描述

    2. 不可以独立使用,需要配置-XX:+PrintGCDetails使用

  • -XX:+PrintGCDateStamps

    1. 输出GC发生的时间戳。发生GC时的时间戳
      在这里插入图片描述

    2. 不可以独立使用,需要配置-XX:+PrintGCDetails使用

  • -XX:+PrintHeapAtGC,每一次GC前和GC后,都打印信息,可以独立使用

  • -Xloggc:,把GC日志写入到一个文件中,而不是打印到标准输出中

  • -XX:+UseGCLogFileRotation,启动GC日志进行滚动

  • -XX:NumberOfGClogFiles=1,GC日志文件的循环数目

  • -XX:GCLogFileSize=1M,当GC日志文件达到多大时,进行日志滚动

如果需要对GC日志进行滚动切割,可以使用logrotate切割GC日志

1.3.3.5.2、其他参数
  • -XX:+TraceClassLoading,监控类的加载
  • -XX:+PrintGCApplicationStoppedTime,打印GC时线程的停顿时间
  • -XX:+PrintGCApplicationConcurrentTime,垃圾收集之前打印出应用未中断的执行时间
  • -XX:+PrintReferenceGC,记录回收了多少种不同引用类型的引用
  • -XX:+PrintTenuringDistribution,打印每次MinorGC后当前使用的Survivor中对象的年龄分布
1.3.3.6、其他参数
  • -XX:+DisableExplicitGC,禁止执行System.gc(),默认禁用
  • -XX:ReservedCodeCacheSize=[g|m|k]、-XX:InitialCodeCacheSize=[g|m|k],指定代码缓存的大小
  • -XX:+UseCodeCacheFlushing,让JVM放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况
  • -XX:+DoEscapeAnalysis,开启逃逸分析
  • -XX:+UseBiasedLocking,开启偏向锁
  • -XX:+UseTLAB,使用TLAB,默认打开
  • -XX:+PrintTLAB,打印TLAB的使用情况
  • -XX:TLABSize,设置TLAB大小
1.3.4、通过Java代码获取JVM参数,可以使用RunTime类获取相关参数

1.4、分析GC日志

1.4.1、GC日志参数
  • -verbose:gc,输出GC日志信息,默认输出到标准输出中
  • -XX:+PrintGC,等同于-verbose:gc
  • -XX:+PrintGCDetails,在发生垃圾回收时打印内存回收详细信息,并且在进程退出时输出内存各区域的使用情况
  • -XX:+PrintGCTimeStamps,输出GC发生的时间戳。到发生GC时,程序应该运行了多少秒
  • -XX:+PrintGCDateStamps,输出GC发生的时间戳。发生GC时的时间戳
  • -XX:+PrintHeapAtGC,每一次GC前和GC后,都打印信息
  • -Xloggc:,把GC日志写入到一个文件中,而不是打印到标准输出中
  • -XX:+UseSerialGC,启用Serial + Serial Old垃圾收集器
  • -XX:+UseConcMarkSweepGC,启用ParNew(Young)+ CMS(Old)+ Serial Old(后备方案)的组合
  • -XX:+UseParallelGC,启用Parallel Scavenge + Parallel Old垃圾收集器
  • -XX:+UseG1GC,启用G1垃圾收集器
1.4.2、GC分类

针对HotSpot VM的实现,它里面的GC按照回收区域分为两大种类型:一种部分收集(Partial GC),一种整堆收集(Full GC)

  • 部分收集

    1. 新生代收集(Minor GC|Young GC),只是新生代(Eden、S0、S1)的垃圾收集
    2. 老年代收集(Major GC|Old GC),只是老年代的垃圾收集,目前只有CMS GC会有单独收集老年代的行为,注意,很多时候Major GC和Full GC混淆使用,需要根据上下文具体分辨是老年代还是整堆回收
    3. 混合收集(Mixed GC),收集整个新生代以及部分老年代,目前只有G1 GC会有这种行为
  • 整堆收集(Full GC),收集整个Java堆和方法区

1.4.3、Young GC和Old GC的一般格式
  • GC类型和GC发生的原因
  • 垃圾收集器
  • GC发生的内存区域
  • GC前后情况区域的使用情况
  • GC前后堆内存的使用情况
  • GC时间
1.4.4、Serial、ParNew、Parallel的Young GC和Full GC日志(分代且没有使用region)
1.4.4.1、Young GC日志解析

JVM系列之故障排查与性能调优(重点)_第76张图片

  • 2021-02-22T16:48:58.733+0800,-XX:+PrintGCDateStamps参数控制,打印GC发生时的时间戳
  • 0.197,-XX:+PrintGCTimeStamps参数控制,GC发生时,JVM虚拟机启动以来经过的秒数,单位秒
  • GC (Allocation Failure),发生了一次垃圾回收,括号里面是发生本次GC的原因,这里的Allocation Failure是指新生代内存不足,不能够为新对象分配内存
  • [PSYoungGen: 46137K->776K(59904K)],
    1. PSYoungGen:表示GC发生的区域,区域名称与使用的垃圾收集器是密切相关的
    2. Serial收集器:Default New Generation,显示DefNew
    3. ParNew收集器:ParNew
    4. Parallel Scavenge收集器:PSYoung
    5. G1收集器:garbage-first heap
    6. 老年代和新生代同理,也是和收集器名称相关
    7. 46137K->41744K(196608K),GC前新生代使用大小 -> GC后新生代使用大小(新生代总大小),新生代(Eden + 1个Survivor)
  • 46137K->41744K(196608K),YoungGC前堆内存占用 -> YoungGC后堆内存占用(堆内存总大小)
  • 0.0088510 secs,整个GC所花费的时间,单位秒
  • [Times: user=0.00 sys=0.00, real=0.01 secs]
    1. user,指CPU工作在用户态所花费的时间
    2. sys,指CPU工作在内核态所花费的时间
    3. real,此次GC所花费的总时间
      JVM系列之故障排查与性能调优(重点)_第77张图片
1.4.4.2、Full GC日志解析

JVM系列之故障排查与性能调优(重点)_第78张图片

  • 2021-02-22T16:48:58.766+0800,-XX:+PrintGCDateStamps参数控制,打印GC发生时的时间戳
  • 0.230,-XX:+PrintGCTimeStamps参数控制,GC发生时,JVM虚拟机启动以来经过的秒数,单位秒
  • Full GC (Ergonomics)
    1. 表示这是一次Full GC,整堆回收
    2. 回收新生代、老年代、方法区等
    3. Full GC (Ergonomics)JVM自适应调整导致的GC
    4. Full GC (Metadata GC Threshold)元空间发生了GC
    5. Full GC(System)调用了System.gc()方法导致的GC
  • [PSYoungGen: 664K->0K(59904K)]
    1. PSYoungGen:表示GC发生的区域,区域名称与使用的垃圾收集器是密切相关的
    2. Serial收集器:Default New Generation,显示DefNew
    3. ParNew收集器:ParNew
    4. Parallel Scavenge收集器:PSYoung
    5. 老年代和新生代同理,也是和收集器名称相关
    6. 664K->0K(59904K),GC前新生代使用大小 -> GC后新生代使用大小(新生代总大小),新生代 = Eden + 1个Survivor
  • [ParOldGen: 122888K->123491K(136704K)],GC前老年代使用大小 -> GC后老年代使用大小(老年代总大小)
  • 123552K->123491K(196608K)
    1. GC前堆内存使用大小 -> GC后堆内存使用大小(堆内存总大小)
    2. 堆内存总容量 = 新生代(Eden + 1个Survivor)+ 老年代
    3. 单位千字节
  • [Metaspace: 3149K->3149K(1056768K)],GC前元空间使用大小 -> GC后元空间使用大小(堆内存总大小)
  • 0.0224613 secs,整个GC所花费的时间,单位秒
  • [Times: user=0.06 sys=0.00, real=0.02 secs]
    1. user,指CPU工作在用户态所花费的时间
    2. sys,指CPU工作在内核态所花费的时间
    3. real,此次GC所花费的总时间

JVM系列之故障排查与性能调优(重点)_第79张图片

1.4.5、CMS的Old GC日志

CMS

1.4.6、G1的GC日志

G1的GC

1.4.7、ZGC的GC日志

ZGC的GC

1.4.8、GC日志分析工具

GCEasy,通过GC日志可视化分析工具,我们可以很方便的看到JVM各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等,将GC日志上传到GCEasy中,点击分析即可。

1.5、故障排查

1.5.1、如何排查内存泄漏

内存泄漏是指程序申请并使用内存后一直不释放,内存一直占用着。一次泄露不会有明显影响,累积泄露会导致OOM(内存溢出),内存泄漏是导致内存溢出的原因之一,如果应用周期性发布,重新部署,内存泄漏可能就非常隐蔽,难以发现,内存泄漏是由于代码不善引起的,要注意程序一些容易犯错的地方。

内存泄漏的典型现象:

  • 随着时间的推移,老年代内存占用呈现上升趋势,因为泄漏的对象在经历多次GC后肯定会晋升到老年代
  • 每次执行Old GC或者是Full GC后,也就是老年代被回收后,老年代的内存占用在逐步上升。当然了这需要排除用户线程产生对象的干扰,可以在每天零点进行观察老年代内存使用情况

图示是一个博客关于内存泄漏的案例,可以看到老年代内存占用呈现逐步上升趋势,且每次Old GC后,内存占用仍然呈现上升趋势
JVM系列之故障排查与性能调优(重点)_第80张图片
内存泄漏案例

典型内存泄漏用法:

  • 静态成员变量例如ArrayList,add了大量数据,然后没有clear
  • 单例对象大量持有其他对象
  • 数据库连接、网络连接等,及时关闭资源,在finally里面释放,或者用try-with-resources自动关闭
  • 使用ThreadLocal进行set存储数据后,没有进行remove数据
  • 使用复杂对象(多字段)作为HashMap的key存入到Map中或者是复杂对象添加到HashSet
    1. HashMap判断两个对象是否相等会先调用hashCode()方法,然后是equals()方法
    2. 如果只重写hashCode()方法,没有重写equals()方法,则没有起到去重的目的,HashMap或者是HashSet中存在大量重复对象
    3. 同时重写了hashcode()和equals()方法,但是在实现hashCode()方法的代码中,使用的字段在后续程序中进行了修改,那么必然导致内存泄漏

为什么使用String类作为key,就没有发生内存泄漏呢?
因为String类手动重写了hashCode()和equals()方法,并且String类是不可变的,不能进行修改,只会返回一个新的String对象。

1.5.2、如何排查OOM
  • 一般的手段是通过内存映像分析工具(主要是MAT工具),对dump出来的堆转存储快照进行分析,重点确认内存中的对象是否是必要的,先分清楚到底是出现了内存泄露,还是内存溢出
  • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置
  • 如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
1.5.3、CPU使用率过高
  • 原生命令
    1. 使用top命令,找到CPU使用率高对应Java进程的pid
    2. 使用top -Hp ,找到事故线程的tid
    3. 使用jstack | grep $(printf ‘%x’ ) -A 20,dump对应线程方法调用栈后20行
      1. jstack ,dump对应Java进程的线程快照
      2. printf ‘%x’ ,转换成16进制,根据线程的16进制id进行过滤
      3. grep 16进制id -A 20,匹配行的后20行(A:After、B:Before、C:Context)

show-busy-java-threads,显示繁忙的java线程脚本,用于快速排查Java的CPU性能问题(top us值过高),自动查出运行的Java进程中消耗CPU多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。
show-busy-java-threads

arthas的thread -n 线程数命令

1.5.4、由安全点导致长时间停顿

案例分析

1.5.5、线上环境Young GC频繁如何排查
  • Young GC频繁的直接原因是频繁创建对象,且伊甸园区放不下,进行了Young GC,且没有回收对少内存。因为这些对象还存活着,业务过程还没有结束,调大新生代,查看Young GC的执行频率
  • jstat -gccause -t [interval] [count] 观察Young GC发生的次数,以及频率
1.5.6、线上环境Full GC频繁如何排查

直接原因,内存不够用了,JVM频繁调用垃圾收集器回收内存但是回收的内存较少或者根本就没有回收内存,导致Full GC频繁执行,Full GC可能会发生OOM,也可能不会发生OOM

1.5.6.1、排查步骤:

一般发生事故需要先保护现场然后排查问题

  • 保护现场
    1. dump线程快照,jstack Java进程的pid
    2. 查看GC线程CPU使用情况
    3. 打印堆内存中对象数量,jmap -histo:live
    4. 发生Full GC前,dump内存快照,jinfo -flag +HeapDumpBeforeFullGC
    5. 发生OOM后自动dump内存快照,jinfo -flag +HeapDumpOnOutOfMemoryError
    6. 打印各区域内存使用情况,jstat -gc
    7. 打印GC日志,-XX:+PrintGCDetails,-Xloggc:
    8. 查看Full GC发生的原因,jstat -gccause -t
  • 可能原因
    1. 应用访问量突然增加,业务数据生成了大量的对象,堆内存被迅速耗尽,频繁执行Full GC回收内存,设置的JVM内存大小确实不够用。解决方法,调整内存参数,增加Java进程内存大小
    2. 代码存在bug,发生了内存泄露,进而导致了内存溢出,使用MAT工具分析多次dump的内存快照,排查内存中存在的大对象,也就是可能泄露的对象
    3. Metaspace内存溢出,可能是加载了大量的重复Class对象,且没有及时进行卸载,使用MAT工具加载dump文件,点击Duplicate Classes查看重复加载的类
    4. Full GC发生在老年代,年轻代内存设置太小,大量对象晋升到老年代,创建了大量大对象,例如大数组,年轻代放不下,进入老年代

你可能感兴趣的:(面试系列,JVM系列,jvm,java,开发语言)