JVM 故障调查教程
JVM 故障调查教程
java 程序 cpu100%原因排查
第一步:确认cpu占用情况及进程ID
第二步: 显示进程下的线程占用CPU情况
第三步: 导出java进程的线程栈
jstack 9745 > 9745.tdump
#查看tdump
cat 9745.tdump
小结
jvm 内存空间不足引起的CPU100% 原因排除
代码
运行代码:
java -cp demofullgc.jar -XX:+PrintGC -Xms50M -Xmx50M com.hdk.demofullgc.FullGCProblem 结果
- -Xmn 年轻代 -Xms 最小堆 默认是物理内存的1/64; -Xmx 最大堆 默认是物理内存的1/4 –Xss 栈空间,
- -XX:+UseTLAB 使用 TLAB,默认打开
- -XX:+PrintTLAB 打印 TLAB 的使用情况
- -XX:TLABSize 设置 TLAB 大小
- -XX:+DisableExplicitGC 启用用于禁用对的调用处理的选项 System.gc()
- -XX:+PrintGC 查看 GC 基本信息
- -XX:+PrintGCDetails 查看 GC 详细信息
- -XX:+PrintHeapAtGC 每次一次 GC 后,都打印堆信息
- -XX:+PrintGCTimeStamps启用在每个 GC 上打印时间戳的功能
- -XX:+PrintGCApplicationConcurrentTime 打印应用程序时间(低)
- -XX:+PrintGCApplicationStoppedTime 打印暂停时长(低)
- -XX:+PrintReferenceGC 记录回收了多少种不同引用类型的引用(重要性低)
- -verbose:class 类加载详细过程
- -XX:+PrintVMOptions 可在程序运行时,打印虚拟机接受到的命令行显示参数
- -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 打印所有的 JVM 参数、查看所有 JVM 参数启动的初始值(必须会用)
- -XX:MaxTenuringThreshold 升代年龄,最大值 15, 并行(吞吐量)收集器的默认值为 15,而 CMS 收集器的默认值为 6。
运行:top 查看CPU占用情况及pid
Jstat
代码中有打印 GC 参数,生产上可以使用这个 jstat –gc 来统计
分析:jstat-gc 5953 2500 100 需要每 250 毫秒查询一次进程 5953 垃圾收集状况,一共查询 100 次。
随着GCT和GCT逐渐变大,FGC(老年代垃圾回收次数)频率也越来越高。1,1,2,2,3,3,5,6,245。
最后抛出outofmemoryerror异常。CPU占用100%
jmap
分析:ScheduledThreadPoolExecutor$ScheduledFutureTask,UserInfo等对象都高达159956个。主要是类FullGCProblem里的线程池引起的。
小结
内存泄露导致回收率低
内存泄露,引用内存回收率低,而导出内存的可用空间减少,导致内存快速被占满垃圾回收器不断FULLGC。最终CPU100%。
代码
配置 -XX:+HeapDumpOnOutOfMemoryError -Xms500M -Xmx500M
内存不够产生了oom。生成dump 文件java_pid8839.hprof。
mat分析工具
MAT 工具是基于 Eclipse 平台开发的,本身是一个 Java 程序,是一款很好的内存分析工具。
总结:
在 JVM 出现性能问题的时候。(表现上是 CPU100%,内存一直占用)
- 1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
- 2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,我的是一个很简单的代码,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因,就是我们前面讲过的可达性分析算法,JVM 的内存区域,还有垃圾回收器的基础。
CPU占用过高常见原因
- 超大对象 代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OO
- 超过预期访问量 通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。 比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM。 过多使用 Finalizer
- 过度使用终结器(Finalizer) 对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此 它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError 异常。
- 内存泄漏 大量对象引用没有释放,JVM 无法对其自动回收。 长生命周期的对象持有短生命周期对象的引用 例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
- 变量作用域不合理: 1.一个变量的定义的作用范围大于其使用范围,2.没有及时地把对象设置为 null。
- 内部类持有外部类: Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作) 解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;
- Hash 值改变
- 在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
CPU占用高的解决策略
- 第一步:程序优化。效果通常非常大;一般运行中的程序突然产生CPU占用高,都是因为代码问题引起的OOM。如果没彻底解决代码问题,服务多少资源都不够用。
- 第二步:扩容。一般这种情况,出现在超过预期的访问或业务慢慢地增加,导致资源占用过高。
- 第三步: 参数调优。最后才是考虑调优,通过成本、吞吐量、延迟之间找一个平衡点进行jvm的配置调优。
命令行工具
JPS
列出当前机器上正在运行的虚拟机进程,JPS 从操作系统的临时目录上去找(所以有一些信息可能显示不全)
-m:输出主函数传入的参数. 下的 hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整 package 名称或 jar 完整名称. -v: 列出 jvm 参数, -Xms20m -Xmx50m
jstat
-printcompilation (HotSpot 编译统计)
jinfo
VM 参数分类
JVM 的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
jmap
Heap Configuration: ##堆配置情况,也就是 JVM 参数配置的结果[平常说的 tomcat 配置 JVM 参数,就是在配置这些]
MinHeapFreeRatio = 40 ##最小堆使用比例
MaxHeapFreeRatio = 70 ##最大堆可用比例
MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空间大小
NewSize = 268435456 (256.0MB) ##新生代分配大小
MaxNewSize = 268435456 (256.0MB) ##最大可新生代分配大小
OldSize = 5439488 (5.1875MB) ##老年代大小
SurvivorRatio = 8 ##新生代与 suvivor 的比例
PermSize = 134217728 (128.0MB) ##perm 区 永久代大小
MaxPermSize = 134217728 (128.0MB) ##最大可分配 perm 区 也就是永久代大小
Heap Usage: ##堆使用情况【堆内存实际的使用情况】
New Generation (Eden + 1 Survivor Space): ##新生代(伊甸区 Eden 区 + 幸存区 survior(1+2)空间)
capacity = 241631232 (230.4375MB) ##伊甸区容量
used = 77776272 (74.17323303222656MB) ##已经使用大小
free = 163854960 (156.26426696777344MB) ##剩余容量
32.188004570534986% used ##使用比例
capacity = 214827008 (204.875MB) ##伊甸区容量
used = 74442288 (70.99369812011719MB) ##伊甸区使用
free = 140384720 (133.8813018798828MB) ##伊甸区当前剩余容量
34.65220164496263% used ##伊甸区使用情况
capacity = 26804224 (25.5625MB) ##survior1 区容量
used = 3333984 (3.179534912109375MB) ##surviror1 区已使用情
free = 23470240 (22.382965087890625MB) ##surviror1 区剩余容量
12.43827838477995% used ##survior1 区使用比例
capacity = 26804224 (25.5625MB) ##survior2 区容量
used = 0 (0.0MB) ##survior2 区已使用情况
free = 26804224 (25.5625MB) ##survior2 区剩余容量
capacity = 1879048192 (1792.0MB) ##老年代容量
used = 30847928 (29.41887664794922MB) ##老年代已使用容量
free = 1848200264 (1762.5811233520508MB) ##老年代剩余容量
1.6416783843721663% used ##老年代使用比例
-histo 打印每个 class 的实例数目,内存占用,类全名信息.
jmap –histo jmap –histo:live 如果 live 子参数加上后,只统计活的对象数量
jmap –histo 1196 | head -20 (这样只会显示排名前 20 的数据 )
jmap -dump:live,format=b,file=heap.bin
Sun JDK 提供 jhat(JVM Heap Analysis Tool)命令与 jmap 搭配使用,来分析 jmap
jhat
jhat dump 文件名 后屏幕显示"Server is ready."的提示后,用户在浏览器中键入 http://localhost:7000/就可以访问详情
使用 jhat 可以在服务器上生成堆转储文件分析(一般不推荐,毕竟占用服务器的资源,,比如一个文件就有 1 个 G 的话就需要大约吃一个 1G 的内存资源)
jstack
Arthas
官方文档参考 https://alibaba.github.io/arthas/ Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断
可视化工具
Jconsole,visualvm 这两款使用比较简单,一般java应用都是运行在linux平台。所以这里忽略了。
命令工具总结
调优之前开启、调优之后关闭 -XX:+PrintGC 调试跟踪之打印简单的 GC 信息参数:
** 考虑使用** -XX:+PrintHeapAtGC, 打印推信息 参数设置: -XX:+PrintHeapAtGC应用场景: 获取 Heap 在每次垃圾回收前后的使用状况
-XX:+TraceClassLoading参数方法: -XX:+TraceClassLoading
应用场景:在系统控制台信息中看到 class 加载的过程和具体的 class 信息,可用以分析类的加载顺序以及是否可进行精简操作。
-XX:+DisableExplicitGC 禁止在运行期显式地调用 System.gc()
调优经验分享
添加配置 -Xms1500m -Xmx1500m 增加堆内存空间 内存比例 内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。堆内存不足,会增加 MinorGC ,影响系统性能。
MinorGC比较频发可以通过-Xmn 增加年轻代大小,降低 Minor GC 的频率 。-XX:SurvivorRatio调整大survivor区来减少触发动态年龄判断。
-Xmn1000m -XX:SurvivorRatio=7 修改合适的大小。
-XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128 M 设置一个够用值
元空间一般启动后就不会有太多的变化,所以把MetaspaceSize和MaxMetaspaceSize设置成一样。我们可以设定为 128M,节约内存
吞吐量 频繁的 GC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。
-XX:ParallelGCThreads=8 线程数可以根据你的服务器资源情况来设定(要速度快的话可以设置大点,根据 CPU 的情况来定,一般设置成 CPU 的整 数倍
延时 JVM 的 GC 持续时间也会影响到每次请求的响应时间。
-XX:+UseConcMarkSweepGC 如果是业务响应时间优先的,所以还是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器。
- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,新生代收集发生的频率也是最小的.同时,减少到达老年代的对象.
- 吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用.
- 避免设置过小.当新生代设置过小时会导致:1.MinorGC 次数更加频繁 2.可能导致 MinorGC 对象直接进入老年代,如果此时老年代满了,会触发 FullGC.
- 响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;
- 如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在新生代和老年代回收上的时间比例。
- 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象
- 吞吐量: 这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时+GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
- 停顿时间: 指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
- 垃圾回收频率: 通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可
通过 JVM 参数预先设置 GC 日志,几种 JVM 参数设置如下:
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs jvm-1.0-SNAPSHOT.jar
日志查看工具gcViewer,Gceasy https://gceasy.io/,
GC 调优策略
- 降低 Minor GC 频率
- 由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。 单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
- 降低 Full GC 的频率:
- 由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销 。
- 减少创建大对象:在平常的业务场景中,我们一次性从数据库中查询出一个大对象用于 web 端显示。比如,一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老 年代。这种大对象很容易产生较多的 Full GC。
- 增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。
- 选择合适的 GC 回收器: 如果要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,堆内存比较小的情况下(<6G)选择 CMS(Concurrent Mark Sweep)回收器和堆内存比较大的情况下(>8G)G1 回收器。
- GC调优小结 GC 调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的,一般调优的思路都是"测试 - 分析 - 调优",任何调优都需要结合场景,明确已知问题和性能目标,不能为了调优而调优,以免引入新的 Bug,带来风险和弊端。