每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• Java 开发基本都接触过 Heap Dump
• 并且很多人都会打开 HeapDumpOnOutOfMemoryError
• 先说说 OutOfMemoryError ,很多情况会导致 Java 应用抛出 OutOfMemoryError (参考: https://
zhuanlan.zhihu.com/p/26 5039643 ),但是哪些会触发 HeapDumpOnOutOfMemoryError
• OutOfMemoryError: Java heap space 和 OutOfMemoryError: GC overhead limit exceeded :这两个都是 Java 对
象堆内存不够了,一个是分配的时候发现剩余空间不足,一个是到达某一界限。这两个都会触发
HeapDumpOnOutOfMemoryError
• OutOfMemoryError: unable to create native thread :无法创建新的平台线程,这个不会触发
HeapDumpOnOutOfMemoryError
• OutOfMemoryError: Requested array size exceeds VM limit :当申请的数组大小超过堆内存限制,就会抛出这个异常。
这个会触发 HeapDumpOnOutOfMemoryError
• OutOfMemoryError: Compressed class space 和 OutOfMemoryError: Metaspace :这两个都和元空间相关(底层
原理说明参考: https://juejin.cn/post/7225879724545835045 ),这两个都会触发 HeapDumpOnOutOfMemoryError
• OutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx) :在
DirectByteBuffer 中,首先向 Bits 类申请额度, Bits 类有一个全局的 totalCapacity 变量,记录着全部
DirectByteBuffer 的总大小,每次申请,都先看看是否超限,可用 -XX:MaxDirectMemorySize 限制。这个不会触发
HeapDumpOnOutOfMemoryError
• OutOfMemoryError: map failed :这个是 File MMAP (文件映射内存)时,如果系统内存不足,就会抛出这个异常。这
个不会触发 HeapDumpOnOutOfMemoryError
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• Java 开发基本都接触过 Heap Dump
• 并且很多人都会打开 HeapDumpOnOutOfMemoryError
• 先说说 OutOfMemoryError ,很多情况会导致 Java 应用抛出 OutOfMemoryError (参考:
https://zhuanlan.zhihu.com/p/265039643 ),但是哪些会触发 HeapDumpOnOutOfMemoryError
• 还有一些其他的:
• Shenandoah 分配区域位图,内存的时候,触发的 OutOfMemoryError ,这个会触发
HeapDumpOnOutOfMemoryError 。
• OutOfMemoryError: Native heap allocation failed ,这个 Message 可能不同操作系统不一样,但是一般都有
native heap 。这个就和 Java 对象堆一般没关系,而是其他块内存无法申请导致的,这些不会触发
HeapDumpOnOutOfMemoryError
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 为什么 UP 主不建议打开 HeapDumpOnOutOfMemoryError
• HeapDumpOnOutOfMemoryError 的原理:
• 进入安全点,所有应用线程暂停,针对 HeapDumpOnOutOfMemoryError ,单线程(如果是 jcmd jmap 可以多线
程) dump 堆为线程个数个文件。退出安全点。
• 将上面的多个文件,合并为一个,压缩。
• 这里的瓶颈主要在于第一步写入,并且,主要瓶颈在磁盘 IO ,我们来看下现在云服务的磁盘 IO 标
准:
• AWS EFS (普通存储): https://docs.aws.amazon.com/efs/latest/ug/performance.html
• AWS EBS (对标 SSD ): https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html
• 对于一个 4G 大小的 Java 对象堆内存,如果是 EFS ,对标的应该是 100G 以内的磁盘,写入最少
也需要大概 4 * 1024 / 300 = 13.65 秒(注意,这个是峰值性能),如果当时峰值性能被用完了,那
么需要: 4 * 1024 / 15 = 273 秒。如果用 EBS ,那么也需要 4 * 1024 / 1000 = 4 秒。注意,这个
计算的时间,是应用线程个完全处于安全点(即 Stop-the-world )的时间,还没有还是没考虑一个
机器上部署多个容器实例的情况,考虑成本我们也不能堆每个微服务都使用 AWS EBS 这种(对标
SSD )。
• 所以,建议还是不要打开 HeapDumpOnOutOfMemoryError
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 为什么 UP 主其实觉得 90% 以上的内存泄漏问题没必要 Heap Dump 就能通过 JFR 定位到
• Java 对象内存泄漏有哪些情况:
• 大对象分配导致的问题:
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫
茫众多的请求中很难找到这个请求。
• 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有 bug ,用户订单列表实际是拉取每个用户的所有订单
内存分页。可能两个大历史订单量的用户同时查询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。
• 小对象分配导致的问题:
• 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收,日积月累导致 FullGC 越来越频繁,最后
OutOfMemoryError
• 问题 4 :由于虚拟线程的引入,原来进程内处理请求的数量一定程度受限于 IO 以及线程数量,现在则是受限于 Java 对象堆内存大小,如何识别这种“背
压”问题。
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
接下来你可能有很多疑问,比如 JFR 事件是啥时候什么触发采集的,还有字段意义,不用着急,这些
事件在后面的系列会挨个详细分析
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 测试环境准备
• JDK 21 以上的版本(因为得弄虚拟线程)
• Jmeter ,用来压测模拟真实请求环境
• Spring PetClinic 代码: https://github.com/spring-projects/spring-petclinic.git
• git clone 下来代码之后,先用 Jmeter 将其中的测试脚本打开:
• 脚本目录是:
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 测试环境准备
• JDK 21 以上的版本(因为得弄虚拟线程)
• Jmeter ,用来压测模拟真实请求环境
• Spring PetClinic 代码: https://github.com/spring-projects/spring-petclinic.git
• git clone 下来代码之后,先用 Jmeter 将其中的测试脚本打开:
• 打开后,将线程数量调低一些,调成 50 就够了,并且将循环次数调整为无限次
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 打开项目根目录的 pom.xml ,修改 Java 语言级别为 21 及以上的版本
• 修改 application.properties 开启虚拟线程( spring.threads.virtual.enabled=true )
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 增加一个会返回大量数据的查询,模拟问题:
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 增加 jfr 采集配置文件,命名为 default.jfc (其实随意,后面的启动参数注意保持一致)
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0">
<event name="jdk.ObjectAllocationOutsideTLAB">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
<event name="jdk.ObjectAllocationSample">
<setting name="enabled" control="object-allocation-enabled">true</setting>
<setting name="throttle" control="allocation-profiling">5/s</setting>
<setting name="stackTrace">true</setting>
</event>
<event name="jdk.AllocationRequiringGC">
<setting name="enabled" control="gc-enabled-high">true</setting>
<setting name="stackTrace">true</setting>
</event>
<event name="jdk.ZAllocationStall">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="threshold">0 ms</setting>
</event>
</configuration>
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 启动应用:
• JVM 参数:
• -Xmx256m
• -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc
• -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 启动应用:
• JVM 参数(这次我们先使用默认的 G1 GC ):
• -Xmx256m
• -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc
• -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256
• 启动 Jmeter 测试
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 启动应用
• 启动 Jmeter 测试
• 调用 oom 接口,可以观察到 OOM 后有大量报错(因为请求还是不断进来,默认的 h2 内存数据
库因为 OOM 导致关闭并且无法恢复)
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 启动应用
• 启动 Jmeter 测试
• 调用 oom 接口,可以观察到 OOM 后有大量报错(因为请求还是不断进来,默认的 h2 内存数据
库因为 OOM 导致关闭并且无法恢复)
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 如何找到出问题的请求?
• 第一种方式:
• 使用 JMC 打开 JFR ,找到 Event Browser (事件浏览器)
• 查看 Allocation Outside TLAB
• 按照 Allocation Size 倒序
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 如何找到出问题的请求?
• 第二种方式:
• 使用 JMC 打开 JFR ,找到 Event Browser (事件浏览器)
• 查看 Allocation Requiring GC
• 按照 Size 倒序
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 如何找到出问题的请求?如果是 ZGC 的情况呢?
• 启动参数:
• -Xmx256m
• -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc
• -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256
• -XX:+UseZGC
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了
OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到
这个请求。
• 如何找到出问题的请求?如果是 ZGC 的情况呢?
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有
bug ,用户订单列表实际是拉取每个用户的所有订单内存分页。可能两个大历史订单量的用户同时查
询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。
• 对于问题 2 ,前面的 JFR 事件也大概率可以采集到,我们可以做一个接口,分配大对象,但是不会
OOM ,然后加入 Jmeter 的压测
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有
bug ,用户订单列表实际是拉取每个用户的所有订单内存分页。可能两个大历史订单量的用户同时查
询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。
• 查看 JFR :
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收,
日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收,
日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError
• 这种情况,可能导致 JFR 事件丢失,但是大概率不影响我们定位问题,因为是一连串的趋势可以看
出来
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收,
日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 问题 4 :由于虚拟线程的引入,原来进程内处理请求的数量一定程度受限于 IO 以及线程数量,现在
则是受限于 Java 对象堆内存大小,如何识别这种“背压”问题。
• 这个情况需要结合分析更多事件,我们放在后面,在详细分析一些事件之后再分析。
每日一 JVM 与 JFR 事件
总集篇 – 你可能没必要 Heap Dump
• 为什么抛出 OutOfMemoryError 的微服务最好下线重启?
• 因为包括 JDK 的源码在内,都没有在每一个分配内存的代码的地方考虑会出现 OutOfMemoryError ,这样会导致代码
状态不一致,例如 hashmap 的 rehash ,如果里面某行抛出 OutOfMemoryError ,前面更新的状态就不对了。
• 还有其他很多库,就不用说了,都很少有 catch Throwable 的,大部分是 catch Exception 的。并且,在每一个分配内
存的代码的地方考虑会出现 OutOfMemoryError 也是不现实的,所以为了防止 OutOfMemoryError 带来意想不到的
一致性问题,还是下线重启比较好。
• 如何实现抛出 OutOfMemoryError 的微服务下线重启?
• 一般通过 -XX:OnOutOfMemoryError="/path/to/script.sh" 指定脚本,脚本执行:
• 微服务的下线
• 微服务的重启
• 针对 spring boot ,可以考虑开启允许本地访问 /actuator/shutdown 来关闭微服务(有群友反应抛出
OutOfMemoryError 的时候调用这个会卡死,这是因为前面说的原因,你可能开启了
HeapDumpOnOutOfMemoryError 导致的️
), k8s 会自动拉起一个新的。

为何大多数情况下无需通过 Heap Dump 定位内存问题,建议使用 JFR(Java Flight Recorder)替代

  • 1.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • Java 开发基本都接触过 Heap Dump • 并且很多人都会打开 HeapDumpOnOutOfMemoryError • 先说说 OutOfMemoryError ,很多情况会导致 Java 应用抛出 OutOfMemoryError (参考: https:// zhuanlan.zhihu.com/p/26 5039643 ),但是哪些会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: Java heap space 和 OutOfMemoryError: GC overhead limit exceeded :这两个都是 Java 对 象堆内存不够了,一个是分配的时候发现剩余空间不足,一个是到达某一界限。这两个都会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: unable to create native thread :无法创建新的平台线程,这个不会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: Requested array size exceeds VM limit :当申请的数组大小超过堆内存限制,就会抛出这个异常。 这个会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: Compressed class space 和 OutOfMemoryError: Metaspace :这两个都和元空间相关(底层 原理说明参考: https://juejin.cn/post/7225879724545835045 ),这两个都会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx) :在 DirectByteBuffer 中,首先向 Bits 类申请额度, Bits 类有一个全局的 totalCapacity 变量,记录着全部 DirectByteBuffer 的总大小,每次申请,都先看看是否超限,可用 -XX:MaxDirectMemorySize 限制。这个不会触发 HeapDumpOnOutOfMemoryError • OutOfMemoryError: map failed :这个是 File MMAP (文件映射内存)时,如果系统内存不足,就会抛出这个异常。这 个不会触发 HeapDumpOnOutOfMemoryError
  • 2.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • Java 开发基本都接触过 Heap Dump • 并且很多人都会打开 HeapDumpOnOutOfMemoryError • 先说说 OutOfMemoryError ,很多情况会导致 Java 应用抛出 OutOfMemoryError (参考: https://zhuanlan.zhihu.com/p/265039643 ),但是哪些会触发 HeapDumpOnOutOfMemoryError • 还有一些其他的: • Shenandoah 分配区域位图,内存的时候,触发的 OutOfMemoryError ,这个会触发 HeapDumpOnOutOfMemoryError 。 • OutOfMemoryError: Native heap allocation failed ,这个 Message 可能不同操作系统不一样,但是一般都有 native heap 。这个就和 Java 对象堆一般没关系,而是其他块内存无法申请导致的,这些不会触发 HeapDumpOnOutOfMemoryError
  • 3.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 为什么 UP 主不建议打开 HeapDumpOnOutOfMemoryError • HeapDumpOnOutOfMemoryError 的原理: • 进入安全点,所有应用线程暂停,针对 HeapDumpOnOutOfMemoryError ,单线程(如果是 jcmd jmap 可以多线 程) dump 堆为线程个数个文件。退出安全点。 • 将上面的多个文件,合并为一个,压缩。 • 这里的瓶颈主要在于第一步写入,并且,主要瓶颈在磁盘 IO ,我们来看下现在云服务的磁盘 IO 标 准: • AWS EFS (普通存储): https://docs.aws.amazon.com/efs/latest/ug/performance.html • AWS EBS (对标 SSD ): https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html • 对于一个 4G 大小的 Java 对象堆内存,如果是 EFS ,对标的应该是 100G 以内的磁盘,写入最少 也需要大概 4 * 1024 / 300 = 13.65 秒(注意,这个是峰值性能),如果当时峰值性能被用完了,那 么需要: 4 * 1024 / 15 = 273 秒。如果用 EBS ,那么也需要 4 * 1024 / 1000 = 4 秒。注意,这个 计算的时间,是应用线程个完全处于安全点(即 Stop-the-world )的时间,还没有还是没考虑一个 机器上部署多个容器实例的情况,考虑成本我们也不能堆每个微服务都使用 AWS EBS 这种(对标 SSD )。 • 所以,建议还是不要打开 HeapDumpOnOutOfMemoryError
  • 4.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 为什么 UP 主其实觉得 90% 以上的内存泄漏问题没必要 Heap Dump 就能通过 JFR 定位到 • Java 对象内存泄漏有哪些情况: • 大对象分配导致的问题: • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫 茫众多的请求中很难找到这个请求。 • 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有 bug ,用户订单列表实际是拉取每个用户的所有订单 内存分页。可能两个大历史订单量的用户同时查询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。 • 小对象分配导致的问题: • 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收,日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError • 问题 4 :由于虚拟线程的引入,原来进程内处理请求的数量一定程度受限于 IO 以及线程数量,现在则是受限于 Java 对象堆内存大小,如何识别这种“背 压”问题。
  • 5.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump 接下来你可能有很多疑问,比如 JFR 事件是啥时候什么触发采集的,还有字段意义,不用着急,这些 事件在后面的系列会挨个详细分析
  • 6.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 测试环境准备 • JDK 21 以上的版本(因为得弄虚拟线程) • Jmeter ,用来压测模拟真实请求环境 • Spring PetClinic 代码: https://github.com/spring-projects/spring-petclinic.git • git clone 下来代码之后,先用 Jmeter 将其中的测试脚本打开: • 脚本目录是:
  • 7.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 测试环境准备 • JDK 21 以上的版本(因为得弄虚拟线程) • Jmeter ,用来压测模拟真实请求环境 • Spring PetClinic 代码: https://github.com/spring-projects/spring-petclinic.git • git clone 下来代码之后,先用 Jmeter 将其中的测试脚本打开: • 打开后,将线程数量调低一些,调成 50 就够了,并且将循环次数调整为无限次
  • 8.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 打开项目根目录的 pom.xml ,修改 Java 语言级别为 21 及以上的版本 • 修改 application.properties 开启虚拟线程( spring.threads.virtual.enabled=true )
  • 9.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 增加一个会返回大量数据的查询,模拟问题:
  • 10.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 增加 jfr 采集配置文件,命名为 default.jfc (其实随意,后面的启动参数注意保持一致) <?xml version="1.0" encoding="UTF-8"?> <configuration version="2.0"> <event name="jdk.ObjectAllocationOutsideTLAB"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> </event> <event name="jdk.ObjectAllocationSample"> <setting name="enabled" control="object-allocation-enabled">true</setting> <setting name="throttle" control="allocation-profiling">5/s</setting> <setting name="stackTrace">true</setting> </event> <event name="jdk.AllocationRequiringGC"> <setting name="enabled" control="gc-enabled-high">true</setting> <setting name="stackTrace">true</setting> </event> <event name="jdk.ZAllocationStall"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> </configuration>
  • 11.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 启动应用: • JVM 参数: • -Xmx256m • -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc • -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256
  • 12.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 启动应用: • JVM 参数(这次我们先使用默认的 G1 GC ): • -Xmx256m • -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc • -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256 • 启动 Jmeter 测试
  • 13.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 启动应用 • 启动 Jmeter 测试 • 调用 oom 接口,可以观察到 OOM 后有大量报错(因为请求还是不断进来,默认的 h2 内存数据 库因为 OOM 导致关闭并且无法恢复)
  • 14.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 启动应用 • 启动 Jmeter 测试 • 调用 oom 接口,可以观察到 OOM 后有大量报错(因为请求还是不断进来,默认的 h2 内存数据 库因为 OOM 导致关闭并且无法恢复)
  • 15.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 如何找到出问题的请求? • 第一种方式: • 使用 JMC 打开 JFR ,找到 Event Browser (事件浏览器) • 查看 Allocation Outside TLAB • 按照 Allocation Size 倒序
  • 16.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 如何找到出问题的请求? • 第二种方式: • 使用 JMC 打开 JFR ,找到 Event Browser (事件浏览器) • 查看 Allocation Requiring GC • 按照 Size 倒序
  • 17.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 如何找到出问题的请求?如果是 ZGC 的情况呢? • 启动参数: • -Xmx256m • -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./default.jfc • -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256 • -XX:+UseZGC
  • 18.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 1 :某个请求有 bug ,导致全表扫描,冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError ,但是这是异常情况,可能无法输出堆栈日志,在茫茫众多的请求中很难找到 这个请求。 • 如何找到出问题的请求?如果是 ZGC 的情况呢?
  • 19.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有 bug ,用户订单列表实际是拉取每个用户的所有订单内存分页。可能两个大历史订单量的用户同时查 询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。 • 对于问题 2 ,前面的 JFR 事件也大概率可以采集到,我们可以做一个接口,分配大对象,但是不会 OOM ,然后加入 Jmeter 的压测
  • 20.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 2 :用户累计订单量随着你的系统成熟越来越多,大历史订单量的用户越来越多。之前的代码有 bug ,用户订单列表实际是拉取每个用户的所有订单内存分页。可能两个大历史订单量的用户同时查 询的时候就会抛出 OutOfMemoryError ,就算不抛出也会频繁 GC 影响性能。 • 查看 JFR :
  • 21.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收, 日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError
  • 22.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收, 日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError • 这种情况,可能导致 JFR 事件丢失,但是大概率不影响我们定位问题,因为是一连串的趋势可以看 出来
  • 23.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 3 :某个请求会触发分配一个小对象放入类似于缓存的地方,但是这个小对象一直没有被回收, 日积月累导致 FullGC 越来越频繁,最后 OutOfMemoryError
  • 24.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 问题 4 :由于虚拟线程的引入,原来进程内处理请求的数量一定程度受限于 IO 以及线程数量,现在 则是受限于 Java 对象堆内存大小,如何识别这种“背压”问题。 • 这个情况需要结合分析更多事件,我们放在后面,在详细分析一些事件之后再分析。
  • 25.
    每日一 JVM 与JFR 事件 总集篇 – 你可能没必要 Heap Dump • 为什么抛出 OutOfMemoryError 的微服务最好下线重启? • 因为包括 JDK 的源码在内,都没有在每一个分配内存的代码的地方考虑会出现 OutOfMemoryError ,这样会导致代码 状态不一致,例如 hashmap 的 rehash ,如果里面某行抛出 OutOfMemoryError ,前面更新的状态就不对了。 • 还有其他很多库,就不用说了,都很少有 catch Throwable 的,大部分是 catch Exception 的。并且,在每一个分配内 存的代码的地方考虑会出现 OutOfMemoryError 也是不现实的,所以为了防止 OutOfMemoryError 带来意想不到的 一致性问题,还是下线重启比较好。 • 如何实现抛出 OutOfMemoryError 的微服务下线重启? • 一般通过 -XX:OnOutOfMemoryError="/path/to/script.sh" 指定脚本,脚本执行: • 微服务的下线 • 微服务的重启 • 针对 spring boot ,可以考虑开启允许本地访问 /actuator/shutdown 来关闭微服务(有群友反应抛出 OutOfMemoryError 的时候调用这个会卡死,这是因为前面说的原因,你可能开启了 HeapDumpOnOutOfMemoryError 导致的️ ), k8s 会自动拉起一个新的。