四、Direct buffer memory
我们使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。
Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
4.1 写个 bug
- ByteBuffer.allocate(capability) 是分配 JVM 堆内存,属于 GC 管辖范围,需要内存拷贝所以速度相对较慢;
- ByteBuffer.allocateDirect(capability) 是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快;
如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。
/** * VM Options:-Xms10m,-Xmx10m,-XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m */ public class DirectBufferMemoryDemo { public static void main(String[] args) { System.out.println("maxDirectMemory is:"+sun.misc.VM.maxDirectMemory() / 1024 / 1024 + "MB"); //ByteBuffer buffer = ByteBuffer.allocate(6*1024*1024); ByteBuffer buffer = ByteBuffer.allocateDirect(6*1024*1024); } }
最大直接内存,默认是电脑内存的 1/4,所以我们设小点,然后使用直接内存超过这个值,就会出现 OOM。
maxDirectMemory is:5MB Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
4.2 解决方案
- Java 只能通过
ByteBuffer.allocateDirect
方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查 - 检查是否直接或间接使用了 NIO,如 netty,jetty 等
- 通过启动参数
-XX:MaxDirectMemorySize
调整 Direct ByteBuffer 的上限值 - 检查 JVM 参数是否有
-XX:+DisableExplicitGC
选项,如果有就去掉,因为该参数会使System.gc()
失效 - 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用
sun.misc.Cleaner
的clean()
方法来主动释放被 Direct ByteBuffer 持有的内存空间 - 内存容量确实不足,升级配置
五、Unable to create new native thread
每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
5.1 写个 bug
public static void main(String[] args) { while(true){ new Thread(() -> { try { Thread.sleep(Integer.MAX_VALUE); } catch(InterruptedException e) { } }).start(); } }
Error occurred during initialization of VM java.lang.OutOfMemoryError: unable to create new native thread
5.2 原因分析
JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread
,常见的原因包括以下几类:
- 线程数超过操作系统最大线程数限制(和平台有关)
- 线程数超过 kernel.pid_max(只能重启)
- native 内存不足;该问题发生的常见过程主要包括以下几步:
- JVM 内部的应用程序请求创建一个新的 Java 线程;
- JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
- 操作系统尝试创建一个新的 native 线程,并为其分配内存;
- 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
- JVM 将抛出
java.lang.OutOfMemoryError:Unableto createnewnativethread
错误。
5.3 解决方案
- 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程
- 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行
ulimia-a
查看最大线程数限制,使用ulimit-u xxx
调整最大线程数限制
六、Metaspace
JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。
Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常。
6.1 写个 bug
/** * JVM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m */ public class MetaspaceOOMDemo { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(MetaspaceOOMDemo.class); enhancer.setUseCache(false); enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> { //动态代理创建对象 return methodProxy.invokeSuper(o, objects); }); enhancer.create(); } } }
借助 Spring 的 GCLib 实现动态创建对象
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
6.2 解决方案
方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了上边的 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。
方法区在 JDK8 中一般不太容易产生,HotSpot 提供了一些参数来设置元空间,可以起到预防作用
-XX:MaxMetaspaceSize
设置元空间最大值,默认是 -1,表示不限制(还是要受本地内存大小限制的)-XX:MetaspaceSize
指定元空间的初始空间大小,以字节为单位,达到该值就会触发 GC 进行类型卸载,同时收集器会对该值进行调整-XX:MinMetaspaceFreeRatio
在 GC 之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集频率,类似的还有MaxMetaspaceFreeRatio
七、Requested array size exceeds VM limit
7.1 写个 bug
public static void main(String[] args) { int[] arr = new int[Integer.MAX_VALUE]; }
这个比较简单,建个超级大数组就会出现 OOM,不多说了
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。
JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2
。
此类问题比较罕见,通常需要检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。
八、Out of swap space
启动 Java 应用程序会分配有限的内存。此限制是通过-Xmx和其他类似的启动参数指定的。
在 JVM 请求的总内存大于可用物理内存的情况下,操作系统开始将内容从内存换出到硬盘驱动器。
该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。
这种错误没见过~~~
九、Kill process or sacrifice child
操作系统是建立在流程概念之上的。这些进程由几个内核作业负责,其中一个名为“ Out of memory Killer”,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。
不同于其他的 OOM 错误, Killprocessorsacrifice child
错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
9.1 原因分析
默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。
9.2 解决方案
- 升级服务器配置/隔离部署,避免争用
- OOM Killer 调优。
最后附上一张“涯海”大神的图
涯海
参考与感谢
《深入理解 Java 虚拟机 第 3 版》
https://plumbr.io/outofmemoryerror
https://yq.aliyun.com/articles/711191
https://github.com/StabilityMan/StabilityGuide/blob/master/docs/diagnosis/jvm/exception