# JVM实战:哪些区域会发生内存溢出?

在这里插入图片描述

# 介绍

在 Java 虚拟机(JVM)中,除了程序计数器不会发生任何 OutOfMemoryError(简称 OOM)之外,其他的运行时数据区域在特定情况下都有可能发生内存溢出

# 堆溢出

报错信息:java.lang.OutOfMemoryError: Java heap space

原因:堆是 JVM 中最大的一块内存,用来存储对象实例和数组。如果程序不断创建对象,且这些对象一直被强引用(无法被垃圾回收器回收),当堆内存被撑满并达到最大值(-Xmx)时,就会触发堆溢出

常见场景:内存泄漏,一次性从数据库查询了过多的数据

# 虚拟机栈和本地方法栈溢出

这两块区域负责 Java 方法和本地(Native)方法的调用,主要会发生两种类型的错误

# 栈溢出(StackOverflowError)

原因:线程请求的栈深度大于虚拟机所允许的深度。每个方法调用都会形成一个栈帧,如果栈帧不停地压入栈中而不弹出,就会撑爆栈内存

常见场景:没有出口的递归调用,或者方法调用链路过深

# 栈内存溢出(OutOfMemoryError)

报错信息:java.lang.OutOfMemoryError: unable to create new native thread

原因:如果在拓展栈时无法申请到足够的内存,或者操作系统无法再创建新的线程时就会报错。通常是因为操作系统内存不足,或者设置的单个线程栈大小(-Xss)太大,导致能创建的线程总数变少

# 方法区/元空间溢出

报错信息

JDK 7 及以前(永久代):java.lang.OutOfMemoryError: PermGen space JDK 8 及以后(元空间):java.lang.OutOfMemoryError: Metaspace

原因:方法区(元空间)用于存储类信息(Class)、常量池、方法数据等。如果运行时加载了大量的类,把这块区域撑满了就会溢出

常见场景:使用了 CGLIB 等字节码增强技术动态生成了大量的类、在 Web 应用中频繁地去 Deploy/Undeploy 导致类加载器无法回收、或者大量使用了 String.intern()(在 JDK 6 及以前字符串常量池在永久代)

# 运行时常量池溢出

注意点:这取决于 JDK 的版本。

在 JDK 6 及以前,运行时常量池在永久代,如果大量使用 String.intern() 可能会导致 PermGen space 的 OOM

在 JDK 7 及以后,字符串常量池已经被移到了 Java 堆中,因此它引起的溢出会直接表现为 Java heap space 溢出

# 直接内存溢出

报错信息:java.lang.OutOfMemoryError(通常没有明确的下游提示,或者提示与 Unsafe / ByteBuffer 相关)

原因:直接内存不属于 JVM 运行时数据区,它是通过 NIO 利用 Native 函数库直接分配的堆外内存。它的溢出通常不会向操作系统申请超过物理内存限制的空间,但如果它加上堆内存的总和超过了物理内存限制,就会报错。

常见场景:使用了 Netty、NIO 等频繁进行堆外内存操作的框架,且没有及时释放内存