1. 为什么我们不推荐启用 HeapDumpOnOutOfMemoryError#
1.1. 启用 HeapDumpOnOutOfMemoryError 后,哪些 OutOfMemoryError 实际会触发它?#
这里有个有趣的事情 - 一旦你启用了 HeapDumpOnOutOfMemoryError,并不是每个 OutOfMemoryError 都会实际触发堆转储!让我们分解不同类型的 OutOfMemoryError 异常,看看哪些会配合:
OutOfMemoryError: Java heap space和OutOfMemoryError: GC overhead limit exceeded:这两个都表示 Java 堆内存不足 - 一个在分配时剩余空间不足时发生,另一个达到特定阈值。这两个都会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: unable to create native thread:当系统无法创建新的平台线程时发生。这个不会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: Requested array size exceeds VM limit:当请求的数组大小超过堆内存限制时抛出。这会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: Compressed class space和OutOfMemoryError: Metaspace:两者都与元空间问题相关。两者都会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx):在 DirectByteBuffer 中,系统首先从 Bits 类请求配额,该类维护一个全局 totalCapacity 变量跟踪所有 DirectByteBuffer 大小。你可以使用-XX:MaxDirectMemorySize限制这个。这不会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: map failed:在文件内存映射(MMAP)期间系统内存不足时发生。这不会触发HeapDumpOnOutOfMemoryError
还有一些额外情况:
Shenandoah 分配区域位图内存问题触发
OutOfMemoryError会触发HeapDumpOnOutOfMemoryErrorOutOfMemoryError: Native heap allocation failed:消息可能因操作系统而异,但通常包括"native heap"。这通常与 Java 对象堆无关,而是其他内存分配失败。这些不会触发HeapDumpOnOutOfMemoryError
1.2. 为什么我们建议不要启用 HeapDumpOnOutOfMemoryError#
让我们深入了解 HeapDumpOnOutOfMemoryError 实际如何工作:
JVM 进入 safepoint,暂停所有应用线程。对于 HeapDumpOnOutOfMemoryError,它使用单线程转储(与可以使用多线程的 jcmd/jmap 不同)创建多个文件。然后退出 safepoint。
然后将这些多个文件合并为一个并压缩。
这里的主要瓶颈是第一步 - 写入过程 - 具体来说,是磁盘 I/O 性能。让我们看看一些真实的云存储性能标准:
- 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
对于 4GB 堆,使用 EFS(对应不到 100GB 磁盘),写入至少需要 4 * 1024 / 300 = 13.65 秒(这还是峰值性能!)。如果峰值性能已经在其他地方使用,你需要 4 * 1024 / 15 = 273 秒。即使使用 EBS,你仍然需要 4 * 1024 / 1000 = 4 秒。记住,这是你的应用线程在 stop-the-world 状态下完全冻结的时间!这甚至没有考虑同一台机器上的多个容器实例。从成本角度来看,我们不能给每个微服务 AWS EBS(SSD 等效)存储。
所以我们的建议?完全跳过 HeapDumpOnOutOfMemoryError!
2. 用什么替代 HeapDumpOnOutOfMemoryError?#
2.1. 使用 JFR 进行内存泄漏检测#
当我需要追踪 OutOfMemoryError 问题时,我通常依赖 JFR 的对象分配样本和旧对象样本数据来定位有问题的对象。只有当这些方法没有产生结果时,我才会考虑生成堆转储。
2.2. 为什么遇到 OutOfMemoryError 的微服务应该重启?#
事情是这样的 - 大多数代码,包括 JDK 源代码,不会在每个内存分配点考虑 OutOfMemoryError。这可能导致应用状态不一致。例如,在 HashMap 重新哈希操作期间,如果中途抛出 OutOfMemoryError,之前更新的状态就会损坏。大多数库很少捕获 Throwable - 它们通常只捕获 Exception。
在每个内存分配点处理 OutOfMemoryError 根本不现实。为了防止 OutOfMemoryError 导致的意外一致性问题,最安全的方法是使服务离线并重启它。
2.3. 如何实现遇到 OutOfMemoryError 的微服务的自动重启?#
你可以使用 -XX:OnOutOfMemoryError="/path/to/script.sh" 来指定一个处理以下内容的脚本:
- 优雅的微服务关闭
- 微服务重启
对于 Spring Boot 应用,考虑启用对 /actuator/shutdown 的本地访问以优雅关闭微服务(尽管一些社区成员报告这在发生 OutOfMemoryError 时可能会挂起 - 这可能是由于启用了 HeapDumpOnOutOfMemoryError,如第 1.2 节所述)。Kubernetes 会自动启动一个新实例。



