JVM实战(32)——内存溢出之堆外内存

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

一、简介

本章,我们将讲解一个使用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服务器可能在不停的使用堆外内存,然后堆外空间不足了,此时就抛出了内存溢出异常:

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

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

二、问题分析

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

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

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

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

2.1 jstat分析

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

按道理说,即使因为程序处理过慢,导致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案例也可以看出,我们平时还是要多去了解一些开源框架的底层原理,这样才能在出现问题时直击要点并解决问题。

相关推荐
吴冰_hogan3 小时前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构11 小时前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark14 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉14 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥16 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开16 小时前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构2 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥2 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇2 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm