2023-08-10  阅读(1)
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/97

一、简介

本章,我们将讲解一个使用Jetty作为Web容器的应用的内存溢出问题,该内存溢出问题发生的区域是堆外内存,主要原因是JVM内存区域划分不合理,我们先来看下系统的背景。

1.1 系统背景

生产环境的一个系统发生告警,拿到生产日志后出现如下字样:

    nio handle failed java.lang.OutOfMemoryError: Direct buffer memory
        at org.eclipse.jetty.io.nio.xxxx
        at org.eclipse.jetty.io.nio.xxxx
        at org.eclipse.jetty.io.nio.xxxx

通过日志,我们可以知道是Direct buffer memory这块区域发生了内存溢出异常,而且下面还有一大堆Jetty相关的调用栈。

Direct buffer memory是什么? 我们先来了解下这块区域。

1.2 堆外内存

Direct buffer memory——堆外内存,顾名思义是Java堆内存以外的一块内存区域, 这块区域不受JVM管理,而由操作系统管理 。我们的程序里并没有直接使用堆外内存,而且通过日志中的调用栈看到,是由Jetty引起的。也就是说,Jetty服务器可能在不停的使用堆外内存,然后堆外空间不足了,此时就抛出了内存溢出异常:

202308102133264371.png

Jetty是采Java编写的Web容器,它的一些底层机制要求它需要使用到堆外内存。在Java中,要使用堆外内存,必须要用到DirectByteBuffer这个类,构建DirectByteBuffer对象的同时(DirectByteBuffer对象的引用本身在Java堆分配空间),就会在Java堆以外的内存空间划出一块区域,然后跟DirectByteBuffer对象关联起来:

202308102133272602.png

当DirectByteBuffer对象失去所有引用,被垃圾回收器判定为垃圾对象时,就会在Young GC或Full GC时被回收掉,回收时也会将与它关联的那块堆外内存释放:

202308102133282483.png

二、问题分析

了解了系统的大致情况以及堆外内存的基本原理,我们大致可以推测出正是因为DirectByteBuffer对象长期没有被回收,导致堆外内存被大量占用,从而引发内存溢出。

那么, 什么情况下会出现大量的DirectByteBuffer对象一直存活,导致大量的堆外内存也无法被释放呢? 根据我们之前的学习经验,有三种可能:

  1. 系统承载着超高并发,瞬间大量请求过来,创建了过多的DirectByteBuffer对象,来不及回收掉下一次请求又过来的,导致内存溢出。
  2. 处理请求速度过慢或超时。
  3. JVM中某些区域划分不合理,导致对象大量存活。

根据监控系统的分析,系统的并发度并不高,程序日志显示也没有很多超时,所以很可能是因为JVM内存区域划分不合理或处理请求速度过慢导致的。

2.1 jstat分析

我们通过jstat分析发现,Jetty会不断的创建DirectByteBuffer对象,直到新生代Eden区满了,就会触发Young GC。但是,往往垃圾回收的一瞬间,很多请求还没处理完,所以只有部分DirectByteBuffer对象被回收,存活下来的DirectByteBuffer对象需要转移到Survivor区,但是Survivor区的大小只有10MB!所以,只能将DirectByteBuffer对象转移到老年代:

202308102133290184.png

按道理说,即使因为程序处理过慢,导致Young GC不能回收掉DirectByteBuffer对象,那么DirectByteBuffer对象进入到老年代后,等程序处理完了,下次Full GC时也会被回收掉。但问题就出在了JVM内存空间划分不合理,我们发现系统上线时的JVM配置是这样的:新生代一共200MB左右的空间,其中每个Survivor区就10MB,老年代反而有800MB左右。

Survivor区的空间不足,导致DirectByteBuffer对象进入老年代,随着老年代中的DirectByteBuffer对象越来越多,这些DirectByteBuffer对象关联的堆外内存占用也会越来越多,此时很多老年代中的DirectByteBuffer对象已经是垃圾对象了,但是由于一直没达到触发老年代回收的阈值,所以也就没法Full GC,堆外内存也就是没法释放,最终导致堆外内存溢出。

2.2 SystemGC

Java NIO其实已经考虑到了上述DirectByteBuffer垃圾对象一直无法被回收的问题,它在每次分配堆外内存时,都会调用下System.gc()方法,提醒JVM主动去回收那些没人引用的DirectByteBuffer对象,从而释放其关联的堆外内存。

但是,我们的系统上线时设置了参数-XX:+DisableExplictGC,也就是屏蔽了程序中的System.gc()方法,最终导致了堆外内存溢出的发生。

三、系统优化

分析清楚了原因,主要从两方面进行优化:

  1. 根据系统运行模型,重新合理分配JVM内存,给新生代更多内存,特别是Survivor区,保证能够容纳每次Young GC后的存活对象;
  2. 去掉参数-XX:+DisableExplictGC,让System.gc()生效。

生产环境原则上是要开启-XX:+DisableExplictGC的,但是如果能够保证自己程序里不出现System.gc(),则可以关闭。

四、总结

本章中,我们的案例之所以发生堆外内存溢出,其实是很多因素综合的结果。包括JVM内存划分不合理、处理请求速度较慢、屏蔽了System.gc()。

所以,生产环境一旦发生OOM异常,除去一些程序bug等很明显的原因,往往是比较难排查的,可能是很多因素综合在一起导致了内存异常,我们要做的就是抓住主要矛盾,先按照最基本的优化思路去分析。另外,从这个案例和之前的tomcat案例也可以看出,我们平时还是要多去了解一些开源框架的底层原理,这样才能在出现问题时直击要点并解决问题。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文