1. 要解决的问题
CPU 飚高,内存溢出,频繁 GC
2. CPU 飚高
2.1 定位问题的思路
首先找到 CPU 飚高的那个 Java 进程,因为你的服务器会有多个 JVM 进程。
然后找到那个进程中的 "问题线程",
最后根据线程堆栈信息找到问题代码。最后对代码进行排查。
2.2 具体操作
1. 通过 top 命令找到 CPU 消耗最高的进程,并记住进程 ID。
2. 再次通过 top -Hp [进程 ID] 找到 CPU 消耗最高的线程 ID,并记住线程 ID.
3. 通过 JDK 提供的 jstack 工具 dump 线程堆栈信息到指定文件中。具体命令:jstack -l [进程 ID] >jstack.log。
4. 由于刚刚的线程 ID 是十进制的,而堆栈信息中的线程 ID 是16进制的,因此我们需要将10进制的转换成16进制的,并用这个线程 ID 在堆栈中查找。使用 printf "%x\n" [十进制数字] ,可以将10进制转换成16进制。
5. 通过刚刚转换的16进制数字从堆栈信息里找到对应的线程堆栈。就可以从该堆栈中看出端倪。
2.3 经验总结
1. 查看是否存在死循环, 根据业务进行修复
2. C2 编译器执行编译时也会抢占 CPU, 什么是 C2编译器呢?当 Java 某一段代码执行次数超过10000次(默认)后,就会将该段代码从解释执行改为编译执行,也就是编译成机器码以提高速度。而这个 C2编译器就是做这个的。如何解决呢?项目上线后,可以先通过压测工具进行预热,这样,等用户真正访问的时候,C2编译器就不会干扰应用程序了。
3. GC 线程导致的,那么极有可能是 Full GC ,那么就要进行 GC 的优化
3. 内存问题(内存的问题就是 GC 的问题)排查
3.1 内存溢出
通过加上 -XX:+HeapDumpOnOutOfMemoryError 参数,
该参数作用是:在程序内存溢出时输出 dump 文件,再通过 dump 分析工具进行分析。
3.2 内存没有溢出,但 GC 不健康(复杂)
a. 通常一个健康的 GC 是什么状态呢?
YGC 5秒一次左右,每次不超过50毫秒,FGC 最好没有,CMS GC 一天一次左右。
b. GC 的优化有2个维度,一是频率,二是时长
1. YGC
频率:
我们看YGC,首先看频率,如果 YGC 超过5秒一次,甚至更长,说明系统内存过大,应该缩小容量,如果频率很高,说明 Eden 区过小,可以将 Eden 区增大,但整个新生代的容量应该在堆的 30% - 40%之间,eden,from 和 to 的比例应该在 8:1:1左右,这个比例可根据对象晋升的大小进行调整。
时长:
如果 YGC 时间过长呢?YGC 有2个过程,一个是扫描,一个是复制,通常扫描速度很快,复制速度相比而言要慢一些,如果每次都有大量对象要复制,就会将 STW 时间延长,还有一个情况就是 StringTable ,这个数据结构中存储着 String.intern 方法返回的常连池的引用,YGC 每次都会扫描这个数据结构(HashTable),如果这个数据结构很大,且没有经过 FGC,那么也会拉长 STW 时长,还有一种情况就是操作系统的虚拟内存,当 GC 时正巧操作系统正在交换内存,也会拉长 STW 时长。
2. FGC,FGC 我们只能优化频率,无法优化时长,因为这个时长无法控制。如何优化频率呢?
触发FGC的原因:
1 是 Old 区内存不够,
2 是元数据区内存不够,
3 是 System.gc(),
4 是 jmap 或者 jcmd,
5 是CMS Promotion failed 或者 concurrent mode failure,
6 JVM 基于悲观策略认为这次 YGC 后 Old 区无法容纳晋升的对象,因此取消 YGC,提前 FGC
优化的策略:
1. 通常优化的点是 Old 区内存不够导致 FGC。如果 FGC 后还有大量对象,说明 Old 区过小,应该扩大 Old 区,
2. 如果 FGC 后效果很好,说明 Old 区存在了大量短命的对象,优化的点应该是让这些对象在新生代就被 YGC 掉,
3. 通常的做法是增大新生代,
4. 如果有大而短命的对象,通过参数设置对象的大小,不要让这些对象进入 Old 区,还需要检查晋升年龄是否过小。
5. 如果 YGC 后,有大量对象因为无法进入 Survivor 区从而提前晋升,这时应该增大 Survivor 区,但不宜太大。
c. 需要一些工具知道 GC 的状况
1. jmap, jcmd工具的使用,注意,jmap 和 jcmd dump 文件的时候会触发 FGC ,使用的时候注意场景
2. jstat,该工具可以查看GC 的详细信息,比如eden ,from,to,old 等区域的内存使用情况
3. jinfo,该工具可以查看当前 jvm 使用了哪些参数,并且也可以在不停机的情况下修改参数
4. dump堆内存文件,离线分析
5. jdk11提供了jhsdb工具
3.3 很重要的一点:线上环境一定要带上 GC 日志。如何配置?
参考1:
bash
# 必备
-XX:+PrintGCDetails # 打印详细的GC日志信息。
-XX:+PrintGCDateStamps #在GC日志中添加时间戳。
-XX:+PrintGCID #打印GC的ID信息。
-XX:+PrintTenuringDistribution #打印对象分布 为了分析 GC 时的晋升情况和晋升导致的高暂停,不看对象年龄分布日志怎么行
-XX:+PrintHeapAtGC # GC 后打印堆数据, 每次发生 GC 时,对比一下 GC 前后的堆内存情况,更直观
-XX:+PrintReferenceGC # 强引用/弱引用/软引用/虚引用/finalize 方法万一有问题,不得打印出来看看?
-XX:+PrintGCApplicationStoppedTime # 打印 STW 时间,暂停时间是 GC 最重要的指标,肯定不能少
# 可选
# 打印 safepoint 信息
# 进入STW阶段之前,需要要找到一个合适的 safepoint ,这个指标一样很重要
#(非必选,出现 GC 问题时最好加上此参数调试)
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径,%t带上时间格式
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=100M
参考2(数据中台的后端服务):
bash
-Xms2g #堆内存大小
-Xmx2g #最大堆内存大小
-XX:MetaspaceSize=256m #元空间
-XX:MaxMetaspaceSize=256m #最大元空间
-XX:MaxDirectMemorySize=1g # 设置New I/O(java.nio)direct-buffer allocations的最大大小
-XX:SurvivorRatio=10 #用于设置新生代中Eden区与Survivor区的空间比例
-XX:+UseConcMarkSweepGC
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+ExplicitGCInvokesConcurrent
-Dsun.rmi.dgc.server.gcInterval=2592000000
-Dsun.rmi.dgc.client.gcInterval=2592000000
-XX:ParallelGCThreads=8
-Xloggc:/data/middleLogs/admin-server_gc.log # gc日志配置
-XX:+PrintGCDetails #打印详细的GC日志信息。
-XX:+PrintGCDateStamps #在GC日志中添加时间戳。
-XX:+PrintGCID #打印GC的ID信息。
-XX:+HeapDumpOnOutOfMemoryError # OOM溢出配置
-XX:HeapDumpPath=/admin-server_20230517175814_java.hprof # OOM dump文件存储路径
-Dfile.encoding=UTF-8
-Dproject.name=admin-server
4. STW是什么?
- STW
5. 总结
- 以上是基本操作,仅供参考
- 需要学习更多事故排查技术,比如排查 IO,网络,TCP 连接等等。