37-大厂面试题:什么是内存溢出?在哪些区域会发生内存溢出?请解释下直接内存OOM

简介: 什么是内存溢出?在哪些区域会发生内存溢出?请解释下直接内存OOM

首先我们先回顾下JVM内存模型:


按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

  • 程序计数器:当前线程执行的字节码的行号指示器,线程私有
  • JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
  • 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
  • JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
  • 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。1.8称为Metaspace元空间
  • 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
  • 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。

最常见的OOM情况有以下三种:

  • java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  • java.lang.OutOfMemoryError: PermGen/Metaspace space ------>java永久代/元空间溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出(JDK1.7之前,1.7之后StringTable挪到堆内存中)。
  • java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

我们后续将重点围绕以上三块区域进行重点讲解以及分析,一步一图彻底掌握。

这里我们先将直接内存的概念以及直接内存为什么会发生OOM给大家做一个解释普及。

JVM内存模型之直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是《Java虚拟机规范》 中定义的内存区域。 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现, 所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区(Buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在Java堆和Native堆中来回复制数据。

这里简单给大家回顾下NIO的知识:

我们不使用ByteBuff的方式读取本地磁盘文件,是通过先将磁盘文件读取到系统内存中的缓冲区,而系统中的缓存区Java代码是无法直接操作的,需要借助于本地函数进行copy到 Java内存的缓冲区byte[] 中。

直接内存的划分相当于就是直接在系统内存中开辟出了一块空间,直接供Java代码使用,避免了数据拷贝带来的效率问题

显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存(包括物理内存、 SWAP分区或者分页文件) 大小以及处理器寻址空间的限制, 一般服务器管理员配置虚拟机参数时, 会根据实际内存去设置-Xmx等参数信息, 但经常忽略掉直接内存, 使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制) , 从而导致动态扩展时出现
OutOfMemoryError异常。

因此我们对直接内存做一个小结:

  • 本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
  • 直接内存也可以由 -XX:MaxDirectMemorySize 指定
  • 直接内存申请空间耗费更高的性能
  • 直接内存IO读写的性能要优于普通的堆内存,常见于NIO操作时,用于数据缓冲区

当我们需要频繁访问大的内存而不是申请和释放空间时,通过使用直接内存可以提高性能。

Java创建直接内存

Java中ByteBuffer用于创建内存缓存,其类的继承关系如下:

其中HeapByteBuffer用于创建JVM堆内缓存区,DirectByteBuffer用于创建Native缓存区。通过调用ByteBuffer的静态方法allocateallocateDirect方法分别创建两种缓存区。

MappedByteBuffer是NIO提供的文件内存映射的实现方案,可以把整个文件或文件段映射到Native堆内存。MappedByteBuffer是抽象类,DirectByteBuffer才是提供实际功能的其直接子类,他同时实现了DirectBuffer接口,此接口提供了cleaner用于GC管理。

代码演示

这里我们通过以下代码来演示Java在直接本机内存中进行分配对象与回收:

public class ByteBufferDemo {
   
   
    static int _1Gb=1024*1024*1024;

    public static void main(String[] args) throws IOException {
   
   

        System.out.println("准备申请直接内存前");
        System.in.read();

        //拿到 DirectByteBuffer 这个对象来操作直接内存1G
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配直接内存1G完毕");

        System.in.read();

        System.out.println("开始释放1G直接内存");
        byteBuffer = null;

        System.out.println("开始准备垃圾回收");
        System.in.read();
        //手动触发Full GC
        System.gc();

        System.out.println("垃圾回收完毕");
        System.in.read();
    }
}

1) 程序运行起来,准备申请直接内存前

​ 我们看下我们目前的内存使用:

目前该Java类运行起来后只占据了17.5M

2)通过ByteBuff申请1G直接内存

​ 代码执行:ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); 后内存如下:

瞬间吃掉了内存1G

3)byteBuffer = null;

​ 控制台打印如下:

准备申请直接内存前

分配直接内存1G完毕

开始释放1G直接内存
开始准备垃圾回收

注意这一步仅仅只是将 byteBuff 置为了 null ,内存还并没未被释放!

4)System.gc();

手动触发垃圾回收:

可以看到该行代码执行完毕后,直接内存才真正被释放完毕,回收掉byteBuffer对象

直接内存溢出测试-DirectByteBuffer

DirectByteBuffer在对内存进行操作的时候是提供了对堆外内存的自动回收的

测试代码如下,运行时添加参数 -verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=20M

/**
 * vm参数:-verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=20M
 */
public class ByteBufferOOMTest {
   
   
    static int _1Gb=1024*1024*1024;
    public static void main(String[] args) {
   
   
        while(true){
   
   
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        }
    }
}

控制台输出 OOM : Direct buffer memory!!!

DirectByteBuffer源码分析

通过查看DirectByteBuffer源码可以发现,其实内部底层使用的是 Unsafe类来进行操作的:

通过借助于一个 cleaner线程对象进行垃圾回收,里面提供了一个clean()方法:

该方法用来开启线程的执行,对应的run方法中调用了 UNSAFE的freeMemory() 方法对直接内存进行释放

如果仔细查看Cleaner这个对象 你发现其实该对象使用了虚引用的技术:

public class Cleaner extends PhantomReference<Object> {
   
   
     private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();

在之前讲解对象引用的时候我们专门说过虚引用:

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

直接内存溢出测试-Unsafe

因此我们可以直接使用Unsafe来进行直接内存的申请:

/**
 * vm参数:-verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=20M
 */
public class UnsafeOOMTest {
   
   

    private static final int _1M = 1024 * 1024;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
   
   
        // 通过反射获取rt.jar下的Unsafe类
        Field theUnsafeInstance = Unsafe.class.getDeclaredFields()[0];
        theUnsafeInstance.setAccessible(true);
        // (Unsafe) theUnsafeInstance.get(null);是等价的
        Unsafe unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);

        while(true) {
   
   
            unsafe.allocateMemory(_1M);
        }
    }
}

运行后依然爆出OOM异常:

注意 :使用JDK内部的Unsafe类直接使用堆外内存,JVM是不会自动进行内存回收的! 需要配合unsafe.freeMemory(point); 方法手动进行回收释放

直接内存OOM小结:

当我们想在堆外内存中直接申请空间进行分配时,可以使用 DirectByteBuffer来进行操作,当遇到JVM FullGC的时候会自动进行堆外内存的回收,或者我们手动调用 System.GC() 进行主动回收。

由于堆外内存并没有 JVM 协助管理内存,需要我们自己来管理堆外内存,为了防止内存溢出,避免一直没有 Full GC,最终导致物理内存被耗完,建议大家在配置JVM参数的时候,通过:-XX:MaxDirectMemerySize来指定,当达到阈值的时候,调用system.gc来进行一次full gc,把那些没有被使用的直接内存回收掉。

目录
相关文章
|
11天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
88 1
|
1月前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。
|
2月前
|
缓存 监控 NoSQL
阿里面试让聊一聊Redis 的内存淘汰(驱逐)策略
大家好,我是 V 哥。粉丝小 A 面试阿里时被问到 Redis 的内存淘汰策略问题,特此整理了一份详细笔记供参考。Redis 的内存淘汰策略决定了在内存达到上限时如何移除数据。希望这份笔记对你有所帮助!欢迎关注“威哥爱编程”,一起学习与成长。
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
3月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
2月前
|
Java C++
Java内存区域于内存溢出异常
这篇文章详细解释了Java虚拟机的内存区域划分、各区域的作用以及可能遇到的内存溢出异常情况。
48 0
|
3月前
|
存储 Java 编译器
Java内存区域与内存溢出异常 - 运行时数据区
【8月更文挑战第2天】Java运行时数据区包括:1) 程序计数器:记录线程执行字节码的行号,线程私有;2) Java虚拟机栈:描述方法执行的内存模型,线程私有,深度过大抛出`StackOverflowError`;3) 本地方法栈:服务于Native方法,线程私有;4) Java堆:所有线程共享,对象实例在此分配内存;5) 方法区:存储类信息、常量等数据;6) 运行时常量池:方法区的一部分,存放字面量和符号引用。不当使用如无限创建对象或过度递归调用会导致各种内存溢出错误。
|
4月前
|
Arthas 存储 Java
JVM内存问题之Linux使用ptmalloc2导致的JNI内存溢出问题如何解决
JVM内存问题之Linux使用ptmalloc2导致的JNI内存溢出问题如何解决
|
3月前
|
Java 开发工具 Android开发
Android经典面试题之开发中常见的内存泄漏,以及如何避免和防范
本文介绍Android开发中内存泄漏的概念及其危害,并列举了四种常见泄漏原因:静态变量持有Context、非静态内部类、资源未释放及监听器未注销。提供了具体代码示例和防范措施,如使用ApplicationContext、弱引用、适时释放资源及利用工具检测泄漏。通过遵循这些建议,开发者可以有效提高应用稳定性和性能。
54 0
下一篇
无影云桌面