本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
没看过上篇的请先看上篇:「一览无余」手把手教你 JVM 调优路径------上篇
总所周知,JVM 是进行自动内存管理的,自有一套默认 JVM 参数生效,但是业务天差地别,所以 JVM 默认参数除了在项目初期比较适用以外,随着应用访问量的增加往往会具有很多问题,我们的 JVM 调优也往往是对这些参数进行改动,通过改动参数来让 JVM 更加适配自己的业务应用程序。
作为一个五年 Java 人,我基本上会在每一次面试中被问到 JVM 调优,每年也总有几次在工作中会需要用到 JVM 调优的相关知识,相信 JVM 调优已经是每一个 Java 人必不可少的知识储备了,但是如果现在突然让你对一个 JVM 程序进行调优,你有一套自己从头到尾的逐步流程吗?
在经过上篇的学习之后,我相信大家对进行 JVM 调优的必备知识已经有所了解,本篇就请跟着我的节奏开始逐步学习 JVM 调优:
1. JVM 发生抖动调优
JVM 抖动是最常见的异常情况,但是往往这时候并不会引起许多开发者的注意,因为大家有写不完的业务和开不完的会,所以往往大家调优的时机是应用 pod 发生宕机之后才意识到目前的应用存在问题,才开始进行排查。
但其实如果要防范于未然,是有必要经常看看 JVM 的 GC 监控图的,通过 GC 监控图我们可以很清楚的看到应用是否有发生抖动的风险。
而且许多 OOM 都是因为 JVM 抖动发生的,后续内容将会给出一个例子,所以如果发生了 OOM,我们应该先排查 JVM 是否发生周期性的抖动。
JVM 抖动,大概会有以下三种情况会发生,我将逐个讲解。
Young GC 时间过长
Young GC 发生时间过长,一般 YoungGC 超过100ms,就可以引起警觉了,超过 200ms 就可以导致吞吐量的下降了。
Young区分布的对象都是朝生夕死的对象,也是 GC 最常回收的区域,一旦发生 GC 时间过长就需要拉取相关时间段的 GC 段的日志进行分析,通过 GCeasy工具我们可以很方便的将 GC 日志转换为可视化报表,帮助我们快速分析。
如果这种可视化工具没办法帮你判断出问题点所在,就需要进一步查看 GC 日志,一般来说,Young GC 时间过长你只需要查看垃圾回收之后 Eden 区的变化和 Survivor 区的变化。
如果 Survivor 内存占用极速增加证明证明有相当部分对象没有被回收,可能是代码出现了泄露问题,使大量对象不能被回收只能先进入 Survivor 区,后续可能进入 Old,最终导致 OOM。
如果 Survivor 内存没有增加也没有减少太多,但是 Old 区忽然增加了不少,证明代码中出现了大对象,大对象在放入 Survivor 区的过程中超过了 Survivor 大小,导致其直接升入 Old 区,这种情况会极大增加本次 YoungGC 的时长。可以通过代码修改、调整 Survivor 区占比、修改大对象定义手段酌情处理。
Young GC 频繁
一般垃圾回收之后,需要一点时间才会把 Eden 填满再次触发 Young GC,但是如果你发现 Young GC 过于频繁,可以断定是 Young 区过小或者某段代码操作一次性创建巨量对象,几乎填满 Young 区。
Full GC 频繁
Old 区域不足和元空间满了都会触发 Full GC,此类情况是最严重的抖动情况,因为 Full GC 伴随着 STW,会对整个 JVM 的吞吐量造成较大影响。
元空间是否被填满可以通过 GC 日志或者报告查看,可以先尝试调大元空间大小,然后进行线上观测,在确认应用稳定之后使用 jstat 命令查看其实际用量,然后再进行元空间大小调整。
Old 区空间不足也有两种情况:
- 如果每次 Full GC 带走了大量 Old 区对象,则证明有很多对象发生了过早晋升,过早晋升往往伴随着 YoungGC 时间过长,因为有大量对象没有被回收而是被转移到了 Old 区。
- 第二种情况就是,Old 区真的空间不足了,尝试调大它,或者检查通过堆转储文件检查内存泄露。
2. JVM 无抖动调优
虽然忘记了哪位哲人说过,不要过早优化,但是在某些情况下可以做出过早优化的。
过早优化并不是真的过早,而是使内存使用更具有性价比。
比如你的 Old 区空间很多,99% 的情况都有大量内存空闲,那你就可以尝试调大 Young 区缩小 Old 区,Young 区调大之后就会减少 YoungGC,抖动随之减少。
除此之外,调优的功夫还是下在平时。
比如你的 JVM 监控图一直很平稳,但是平稳并不代表没有问题,通过分析各项监控指标,还是可以做出一些能够减少 GC 时间的优化,这也能够提高你的应用吞吐量。
3. OOM 调优
OOM 调优一般只有两条路可走,一是分析转储文件,二是查看 GC 日志。
分析转储文件这一步一般都是内存泄露了,这种文章比比皆是,我这里不再赘述。
在上一篇我曾经提过,有三个地方容易发生 OOM:堆、直接内存和元空间。
其中元空间的 OOM 是可以直接通过 GC 日志上查看到的,当你在 GC 日志中搜到了带有 Metadata GC Threshold 关键字的日志之后:
shell
[Full GC (Metadata GC Threshold) [PSYoungGen: 10848K->9240K(274944K)] [ParOldGen: 699779K->696143K(699840K)] [Metaspace: 20989K->20989K(1064960K)], 0.0423622 secs]
[Times: user=0.09 sys=0.00, real=0.04 secs]
就可以判断是元空间发生了 OOM,元空间发生了 OOM 可以通过调大元空间参数进行处理。
直接内存的内存泄露比较难以排查,这需要你排查自己的代码,一般情况下你引用的框架是不会发生直接内存泄露的。
你可以通过 jcmd 命令在宿主机中进行 jvm 应用观测直接内存的使用情况,如果一直在膨胀你可以判定是这里发生了问题。
还有一种我遇到的 OOM 是 K8S 的 OOM,这里放下这个案例,但这个案例的本质上是 JDK8 对容器化的支持并不好。
事情是这样的:在我们的核心业务集群中,总是出现 Pod 在运行20 天左右被 K8S 杀掉,K8S 的 kill 原因是 OOM,但是经过同事和我的数次查看 GC 日志和堆转储文件均证明堆和元空间不存在发生 OOM 的迹象。
在同事放弃之后由我接手排查,通过掘金作者张哈希提供的思路,通过 NMT (上一篇的 JVM 参数小节有这个参数)进行内存跟踪,最终发现整个 JVM 占用的直接内存过大,导致堆 + 元空间 + 直接内存超出了 Pod 内存限制,导致 K8S 触发 OOM 将这个 pod 干掉。
最终我的解决方案是调大了 pod 1G 内存,从此再也没有出现这种情况。
这个案例我当时最不解的是,NIO 申请内存一定是经过计算的,不可能申请的内存还要超过 Pod 的余量,后来经过一番查证发现,JDK 8 对于容器化的支持有限,可能会将 Pod 剩余内存识别成宿主机的剩余内存。
后来从 JDK 10 开始引入了容器感知(Container-Awareness)特性,JDK 11 又进一步增强了这个功能。
最后
最后,JVM 内存调优的问题是多种多样的,在调优的过程中,要综合考虑参数、业务场景、垃圾回收器、JDK 版本、硬件等条件,路漫漫其修远兮,吾将上下而求索。
由于本次主题内容过于冗长,我将其分割成了两篇文章同时发布,上篇是六千字的基础知识篇,本篇则是应对 JVM 抖动和 OOM 相关的思路篇,都是干货满满。
感谢大家能看到这,同时也希望大家能对这两篇文章都点点赞,有任何问题都可以在评论区一块讨论,祝有好收获。