JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。

最近读书心得:在建立养成终身成长型思维过程,要重视熵减生活、工作。眼花缭乱的纷杂社会,很容易让我们进入盲目焦虑的状态。坚壁清野,唯清唯静。

     JVM偏重实战经验的面试,面试官开局都喜欢问这个题目,这个题可以直接考察JVM内存模型基础以及候选人的实战经验,可谓一举两得。候选人也许内心万马奔腾,但是这个确实很考验基础,属于半开放的万能考题。根据候选人的履历,可考察很深入、也可以考察比较浅。能问出这样问题的面试官、以及答好这样的问题的候选人,都有一个共性,就是及其重视基础,韧性十足的扫地憎。能耐心答完这个问题,距离满意的offer将大幅接近。

      我们本系列JVM调优目标,除了达成让系统服务运行更流畅、延时更低,也要达到避免OOM的目的。今天重点分析OOM的种类,并结合示例Demo分析总结OOM原因,帮助有缘刷到的同学巩固掌握OOM这块领域。「拉丁解牛说技术,实用至上,坚持用最简洁直白的文字+最少的代码示例分享干货。」

一、内存泄漏与内存溢出的区别

     内存泄漏Memory Leak,通俗的讲,就是有一部分内存空间被无效的持续占用,导致这部分内存无法回收重复利用。比如在《JAVA并发编程系列(12)ThreadLocal就是这么简单》我们说过,ThreadLocal就可能会发生内存泄漏问题。比如内存只有10Mb,但是程序申请分配了4Mb内存空间,程序运行自始至终从没应用到这4Mb无效内存,GC永远回收不到它。导致内存泄漏了4Mb,内存最终可用空间只有6Mb。

     内存溢出Out of Memory(简称OOM),是指超过可用内存大小=内存应用超标爆表。比方说,内存只有10Mb,如果要放11Mb数据,就超出了内存可用大小,发生OOM。

     而今天的主角内存溢出,按之前《JVM进阶调优系列(2)JVM内存区域怎么划分,分别有什么用》说的那样,内存溢出会发生在heap堆内存、Metaspace元数据区、stack栈内存溢出、直接内存DirectMemory溢出四大种类。

二、heap堆内存溢出

      按之前说过堆内存分年轻代+老年代,发生Out Of Memory Error异常,真实场景实质就是GC之后,仍然无法腾出足够可用内存空间分配给新对象。JVM被迫终止运行退出。

2.1 示例代码Demo

      我们模拟内存溢出,通过设置10Mb堆内存,尝试创建6个2Mb的对象(实际5个都分配不了,就OOM)。

* JVM参数:-Xms10m -Xmx10m -Xmn5m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./

* 参数说明:整个堆内存10Mb,其中老年代5Mb,年轻代5Mb,此外:

* -XX:+HeapDumpOnOutOfMemoryError ,内存溢出时导出整个堆信息,让JVM遇到OOM异常时能输出堆内信息;

* -XX:HeapDumpPath=./,内存异常堆数据导出到当前目录。

package lading.java.jvm;
public class Demo005OOM {
    public static void main(String[] args) {
        byte[] obj_2Mb_1 = new byte[2 * 1024 * 1024];
        byte[] obj_2Mb_2 = new byte[2 * 1024 * 1024];
        byte[] obj_2Mb_3 = new byte[2 * 1024 * 1024];
        byte[] obj_2Mb_4 = new byte[2 * 1024 * 1024];
        byte[] obj_2Mb_5 = new byte[2 * 1024 * 1024];
        byte[] obj_2Mb_6 = new byte[2 * 1024 * 1024];
    }
}

2.2 堆内存溢出日志

     运行后,出现了Exception in thread "main" java.lang.OutOfMemoryError: Java heap space异常。

2.3 堆内存溢出原因分析

     OOM的原因只有2个,一个是我们设置的JVM内存太小,不能满足业务系统合理要求。另一个是代码写的有问题,导致内存申请急速膨胀。

2.3.1 JVM内存分配不合理的几种表现

1、整个-Xms分配太小,系统正常运行没多久就OOM。

2、年轻代和老年代分配不合理,比如年轻代和老年代的比例是9:1,YGC后存活对象很多,老年代放不下,发生了OOM。

3、年轻代的S区过小,每次YGC后存活对象都进入老年代,而老年代的对象又无法回收,导致OOM。

2.3.2 代码问题导致OOM的几种表现

1、一次加载过多无价值的数据,导致OOM。比如有个超大表,本次查询只需要其中2列,但是使用了select * from ,加载了很多没必要的数据导致堆内存急速膨胀。

2、代码bug,有无限循环递归。就出现无限在创建新对象导致OOM。

3、内存泄漏导致内存溢出。比如无效的对象,长期无法被回收,而且还在不断新增,日积月累就发生OOM。

     此外,业务量暴增、或者发生雪崩,导致某个服务实例请求大幅上升,让之前评估合理的JVM配置无法适配当前高并发访问导致OOM。

三、stack栈内存溢出

    在内存区域划分专栏里说过,每个线程都有自己的虚拟机栈,当线程执行一个方法时,会为该方法创建栈帧,用来存放方法里的局部变量引用、方法的出口、动态链接等信息。-Xss一般就是512k,或者1Mb。看系统并发能力以及内存情况来设置。方法执行存在递归死循环,或者方法里面有非常多的代码,比如几万行(这种不太可能),线程执行这种方法就一定会出现java.lang.StackOverflowError异常。

3.1 示例代码Demo

    我们通过设置-Xss256k,让某方法递归调用自己,模拟栈内存溢出异常。

完整JVM参数:-Xms10m -Xmx10m -Xmn5m -Xss256k。

package lading.java.jvm;
public class Demo006StackOverError {
    public static int count = 1;
    /**
     * 该方法将无限递归调用自己,类似while(true)
     */
    public static void stackOverDemo() {
        count++;
        stackOverDemo();
    }
    public static void main(String[] args) {
        stackOverDemo();
    }
}

3.2 栈内存溢出日志

     程序运行异常退出,发生了java.lang.StackOverflowError异常。

3.3 栈内存溢出原因分析

     虚拟机栈出现异常,基本就是代码编写不当,导致方法执行被递归深度过大,甚至无限递归。所以代码的重试机制、while(条件)语句、for(条件)、还有方法里的递归,这些地方要多注意是否会出现Stack over flow error问题。

     虚拟机栈的内存大小-Xss其实一般配置1M足够了。如果JVM内存不大和并发不高,设置256k,或者512k也足够用。特殊的数据计算或者涉及要递归,如果合理,可以提高到2Mb大小。

四、Metaspace元数据区溢出

      元数据区,也是多线程共享区域,但是和我们常说的堆内存不是同一个区域。元数据区主要存放的是,.class字节码、运行时常量池、JIT即时编译后的机器码。控制元数据区大小,主要有2个参数,第一个是 MetaspaceSize,可以设置元空间的初始大小。在JVM启动时,就从系统内存申请分配该大小的内存给元数据区。另一个MaxMetaspaceSize,可以设置元空间的最大大小,这个很有必要,因为默认情况下,元空间的大小可以无限申请达到最大物理内存,通过设置该参数来设置元数据空间上限,避免内存泄漏。

4.1 示例代码Demo

     我们通过cglib Enhancer 多次动态加载LadingShare.class,并对它的startWork方法进行增强。模拟元数据区内存溢出场景。元数据可用空间限制仅提供10Mb,JVM参数:-Xms10m -Xmx10m -Xmn5m -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M。

package lading.java.jvm;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class Demo007MetaspaceOOM {
    public static class LadingShare {
        public LadingShare() {
        }
        /**
         * 开始创作
         */
        public void startWork() {
            System.out.println("拉丁解牛说技术,技术分享开始创作ing");
        }
    }
    static class MethodPlus implements MethodInterceptor {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("LadingShare.startWork()创作前,先构思思路。");
            Object result = proxy.invokeSuper(obj, args);
            System.out.println("LadingShare.startWork()创作后,到技术平台发布分享。");
            return result;
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            System.out.println("当前加载" + ++i + "个LadingShare.class");
            //创建Enhancer对象,动态代理LadingShare.class,并对它的startWork()方法新增而外逻辑
            Enhancer enhancer = new Enhancer();
            LadingShare share = new LadingShare();
            enhancer.setSuperclass(share.getClass());
            enhancer.setUseCache(false);//这里很关键,不能用cache,否则不会OutOfMemoryError-->Metaspace
            enhancer.setCallback(new MethodPlus());
            // 创建代理对象
            LadingShare proxy = (LadingShare) enhancer.create();
            // 调用代理对象的方法
            proxy.startWork();
        }
    }
}

4.2 栈内存溢出日志

      抛出了org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace

以及Caused by: java.lang.OutOfMemoryError: Metaspace异常。

4.3 栈内存溢出原因分析

      当我们设置的metaspace空间太小,而项目代码特别多,项目启动的时候就会报元数据内存溢出。另外一种情况就是demo我们示范的,项目运行过程,存在cglib、ByteBuddy等字节码增强或动态代理技术应用,会动态的生成加载类,需要注意是否存在元数据区OOM问题。

五、DirectMemory直接内存溢出

      在JVM堆内存之外,除了有虚拟机栈内存、元数据区,还有一个DirectMemory直接内存区。这个区域的作用是什么呢?「拉丁解牛说技术,实用至上,坚持用最简洁直白的文字+最少的代码示例分享干货。」

      直接内存的设置,主要是java NIO库的需要。NIO对数据读写要求高,且内存需求大,如果直接使用堆内存进行NIO频繁操作,JVM的堆内存很快就被打满,随后发生频繁的GC。而在堆内存之外的直接内存,有读写效率高、内存空间相对独立且容量够大的特点,非常适合NIO库的应用场景。

      如果我们要用到直接内存,可以通过java.nio.ByteBuffer.allocateDirect()进行申请,也可以通过java.nio.DirectByteBuffer操作直接内存。直接内存我们是无法通过jmap查看,只能通过类似top 命令来看它的内存使用情况。

      直接内存的最大可用空间大小,可以通过-XX:MaxDirectMemorySize来限制。当发生FGC后,这部分内存也会被GC回收。如果GC后,新申请的直接内存,大于直接内存可用空间,就会报直接内存OOM。

5.1 示例代码Demo

       这里我们模拟,设置10Mb的直接内存空间,通过nio..ByteBuffer.allocateDirect()不断申请直接内存,最后导致OOM的简单案例。

JVM参数:主要设置10Mb的直接内存。

 -Xms10m -Xmx10m -Xmn5m -XX:MaxDirectMemorySize=10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./

package lading.java.jvm;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
/**
 * 模拟直接内存溢出场景
 */
public class Demo008DirectMemoryOOM {
    public static void main(String[] args) {
        List<ByteBuffer> directMemList = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            //每次申请2Mb 直接内存
            System.out.println("尝试申请第:" + i + "个2Mb的直接内存");
            ByteBuffer directMem2Mb = ByteBuffer.allocateDirect(2 * 1024 * 1024);
            directMemList.add(directMem2Mb);
        }
    }
}

5.2 DirectMemory直接内存溢出日志

     程序发生了java.lang.OutOfMemoryError: Direct buffer memory异常。

5.3 DirectMemory直接内存溢出原因分析

      直接内存的应用,常见的就是NIO,比如Netty框架。出现了DM OOM,一个可能是研发预估不足,没有设置直接内存大小,或者分配的大小不合理。系统上线前,可以通过压测来合理设置-XX:MaxDirectMemorySize参数。

      另一个是没有主动做好回收。研发人员需要注意手工回收这部分内存,比如可以通过DirectBuffer.cleaner().clean();进行回收。像Netty这种框架,他们会对直接内存进行主动充分管理。

六、内存溢出如何分析定位问题

      之前《系列(7)JVM调优监控必备命令、工具集合》有详细分享通过jmap、jhat、GCeasy、Arthas等命令工具进行分析堆内存、GC情况。这个足以对堆内存溢出、虚拟机栈内存溢出问题、以及元数据区的溢出进行全面分析排查。

      唯独直接内存溢出,如何排查分析呢?首先我们一定要设置 -XX:MaxDirectMemorySize参数,否则当程序申请过大直接内存后,会被Docker、系统悄无声息的干掉,将不会留下没有任何痕迹,就很难排查。

     这里devops建设完善的公司,会对系统进行全面的监控,当超过阈值收到告警后,我们可以及时跟进分析服务状态。

     另外,可以通过设置JVM参数-XX:NativeMemoryTracking=summary | detail参数来分析追踪JVM内存情况,这个参数由于导致5%-10%的额外性能开销,一般不启用。设置该参数,启动系统服务后,可以通过命令

   jcmd 【jvm进程id】 VM.native_memory 查看实时内存分配情况。

此外,也可以再增加-XX:+PrintNMTStatistics、-XX:+UnlockDiagnosticVMOptions参数,当启用 NativeMemoryTracking 时,让JVM退出时打印内存使用情况。这样可以帮助排查发生直接内存溢出时的系统情况。

推荐阅读:

1、JVM进阶调优系列(3)堆内存的对象什么时候被回收?

2、JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?

3、JVM进阶调优系列(1)类加载器原理一文讲透

4、JAVA并发编程系列(13)Future、FutureTask异步小王子

相关文章
|
11天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
11天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
15天前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
47 7
|
19天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
18天前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
24 1
|
20天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
19 3
|
20天前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
21天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
44 1
|
21天前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
39 1
|
21天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
168 1