调优实战-内存溢出的定位与分析
首先,对于以下代码如果造成内存溢出该如何进行定位呢?通过 jmap
与 MAT
工具进行定位分析
代码如下:
public class TestJvmOutOfMemory {public static void main(String[] args) {List<Object> list = new ArrayList<>();for (int i = 0; i < 10000000; i++) {StringBuilder str = new StringBuilder();for (int j = 0; j < 1000; j++) {str.append(UUID.randomUUID().toString());}list.add(str.toString());}System.out.println("ok");}
}
设置虚拟机参数如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
再执行上边代码,发现执行之后,发生了内存溢出,并且在当前项目的目录下产生了 java_pid520944.hprof
文件
使用 MAT 工具分析
在 https://eclipse.dev/mat/downloads.php 中下载 MAT 工具,MAT 工具就是用于分析 Java 堆内存的,可以查看内存泄漏以及内存使用情况
下载解压之后,点击 exe 文件启动 MAT 工具,将生成的 hprof
文件拖入即可,那么通过 MAT 工具可以看到
调优实战-高并发场景调优
首先,说明一下业务场景,系统主要与用户交互,并且主要是提供 API 服务,因此对于系统延时比较敏感,存在的问题为,发现该系统在高峰期延时过高,通过监控平台发现以下问题:
- Young GC 比较频繁,每 10 分钟有 50-60 次,峰值达到 400 次
- Full GC 比较频繁,每 1 个小时平均一次,峰值为 10 分钟 5 次
那么首先排除代码层面的问题,之后再来看 JVM 参数配置所存在的问题,项目使用 JDK8,调优前 JVM 参数如下:
# 设置了堆大小为 4G,新生代大小为 1G
-Xms4096M -Xmx4096M -Xmn1024M
# 设置了永久代大小为 512M,但是并不会生效,因为 JDK8 中使用元空间来实现方法区,永久代已经不使用了,因此下边这两个参数没有起作用
-XX:PermSize=512M
-XX:MaxPermSize=512M
存在问题
问题1:未设置垃圾回收器
从配置的 JVM 参数中可以看到,并未指定使用的垃圾回收器,在 JDK8 中默认使用的垃圾回收器为:(可以在命令行通过 java -XX:+PrintCommandLineFlags -version
来查看 JDK 默认的一些配置信息)
- 年轻代使用 Parallel Scavenge
- 老年代使用 Parallel Old
这个组合的垃圾回收器是以 吞吐量优先
的,适合于后台任务型服务器,但是当前服务是与用户进行交互的,因此需要使用 低延迟优先
的垃圾回收器
问题2:年轻代分配不合理
当前系统主要是向外提供 API,那么系统中大多数对象的生命周期都是比较短的,通过 Young GC 都可以进行回收,但是目前的 JVM 配置给堆空间分配了 4G,新生代只有 1G,而新生代又分为 Eden 和 Survivor 区,因此新生代有效大小为 Eden + 一个 Survivor 区,也就是 0.9 G
那么在服务高负载的情况下,新生代中的 Eden + Survivor 区会迅速被占满,进而导致频繁 Young GC,还会引起本应该被 Young GC 回收的垃圾提前晋升到老年代中,导致 Full GC 的频率增加,老年代使用的 Parallel Old 无法与用户线程并发执行进行垃圾回收,因此 STW 时间比较长
问题3:未设置元空间大小
调优前设置了永久代大小,但是 JDK8 中已经废弃了永久代,因此设置永久代大小无效
对于 JDK8 来说,如果不指定元空间的大小,在 64 位操作系统中,默认元空间初始值为 21MB,默认元空间的最大值是系统内存的大小,初始未给定的元空间的大小,因此元空间初始为 21MB,导致 频繁触发 Full GC
来扩张元空间大小
优化方案
首先,针对垃圾回收器,常用的组合如下:
- Parallel Scavenge + Parallel Old:吞吐量优先,适合后台任务型服务
- ParNew + CMS:低延迟优先,适合对延迟时间比较敏感的服务
- G1:JDK9 默认垃圾回收器,兼顾了高吞吐量和低延迟
- ZGC:JDK11 中退出的低延迟垃圾回收器,无论堆空间多大,都可以保证低延迟
因此,对于目前的系统选择 ParNew + CMS 的组合
而元空间大小的设置,可以通过监控查看元空间峰值为多少,也可以通过命令 jstat -gc [进程id]
查看元空间占用在 150MB 左右,因此可以将元空间大小设置为 256MB
对于年轻代的设置,我们可以考虑在堆空间大小不变的情况下,将新生代空间扩展为 0.5 ~ 1 倍,可以分别扩展 0.5 倍、1 倍,再对扩展后的应用进行压测分析,来选择表现性能更好的方案,这里我们就将年轻代扩展 0.5 倍
优化后的参数设置如下:
# 新生代扩展 0.5 倍
-Xms4096M -Xmx4096M -Xmn1536M
# 初始元空间大小设置为 256M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
# 使用 ParNew + CMS 垃圾回收器
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
# CMS 在重新标记阶段,会暂停用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象,因为并发阶段 GC 线程和用户线程是并发执行的,可能有些对象的状态会因为用户线程的执行而变化,因此在重新标记节点需要进行标记修正,重新标记阶段会以新生代中的对象作为 GC Roots 的一部分,通过开启下边这个参数会在重新标记之前先执行一次 YoungGC 可以回收掉大部分的新生代对象,从而减少扫描 GC Roots 的开销
-XX:+CMSScavengeBeforeRemark
优化方案发布
通过灰度发布,选择部分实例上线,当线上实例指标符合预期之后,再进行全量升级