前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事

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

原创
作者头像
拉丁解牛说技术
发布2024-11-15 15:08:33
140
发布2024-11-15 15:08:33
举报
文章被收录于专栏:JVM高手修炼

2024好事接龙,拉丁解牛祝愿所有有缘刷到的同学,好事发生,喜事连连。

  开篇,先推荐一篇实用的文章《Java EasyExcel导出报表内存溢出全解析》,作者是【bug菌】。

  这篇文章作者详细分享easyExcel内存优化最佳实践,尤其是针对大批量数据导出场景,在满足业务需求基础上如何优化解决OOM问题给出系统思路方法并提供代码示例分析,是一篇非常实用的经验分享,特此推荐给大家。同时今天我们也针对堆内存、非堆内存的OOM问题进行全面分析。

-----------------------------开启我们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=./,内存异常堆数据导出到当前目录。

代码语言:txt
复制
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。

代码语言:txt
复制
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。

代码语言:txt
复制
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=./

代码语言:txt
复制
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进阶调优系列(5)CMS回收器通俗演义一文讲透FullGC

2、JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?

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

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

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、内存泄漏与内存溢出的区别
  • 二、heap堆内存溢出
    • 2.1 示例代码Demo
      • 2.2 堆内存溢出日志
        • 2.3 堆内存溢出原因分析
          • 2.3.1 JVM内存分配不合理的几种表现
          • 2.3.2 代码问题导致OOM的几种表现
      • 三、stack栈内存溢出
        • 3.1 示例代码Demo
          • 3.2 栈内存溢出日志
            • 3.3 栈内存溢出原因分析
            • 四、Metaspace元数据区溢出
              • 4.1 示例代码Demo
                • 4.2 栈内存溢出日志
                  • 4.3 栈内存溢出原因分析
                  • 五、DirectMemory直接内存溢出
                    • 5.1 示例代码Demo
                      • 5.2 DirectMemory直接内存溢出日志
                        • 5.3 DirectMemory直接内存溢出原因分析
                        • 六、内存溢出如何分析定位问题
                        相关产品与服务
                        腾讯云 BI
                        腾讯云 BI(Business Intelligence,BI)提供从数据源接入、数据建模到数据可视化分析全流程的BI能力,帮助经营者快速获取决策数据依据。系统采用敏捷自助式设计,使用者仅需通过简单拖拽即可完成原本复杂的报表开发过程,并支持报表的分享、推送等企业协作场景。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档