千万不要这样写代码!9种常见的OOM场景演示(二)

简介: 《Java虚拟机规范》里规定除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能,我们本文就来演示一下这些错误的使用场景。

四、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 解决方案

  1. Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查
  2. 检查是否直接或间接使用了 NIO,如 netty,jetty 等
  3. 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
  4. 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效
  5. 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleanerclean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
  6. 内存容量确实不足,升级配置

五、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 原因分析

98.jpg

JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:

  • 线程数超过操作系统最大线程数限制(和平台有关)
  • 线程数超过 kernel.pid_max(只能重启)
  • native 内存不足;该问题发生的常见过程主要包括以下几步:
  1. JVM 内部的应用程序请求创建一个新的 Java 线程;
  2. JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
  3. 操作系统尝试创建一个新的 native 线程,并为其分配内存;
  4. 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
  5. JVM 将抛出 java.lang.OutOfMemoryError:Unableto createnewnativethread 错误。

5.3 解决方案

  1. 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程
  2. 如果确实需要创建很多线程,调高 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 请求的总内存大于可用物理内存的情况下,操作系统开始将内容从内存换出到硬盘驱动器。

99.jpg

该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。

这种错误没见过~~~

1.gif

九、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 调优。

最后附上一张“涯海”大神的图

100.jpg涯海

参考与感谢

《深入理解 Java 虚拟机 第 3 版》

https://plumbr.io/outofmemoryerror

https://yq.aliyun.com/articles/711191

https://github.com/StabilityMan/StabilityGuide/blob/master/docs/diagnosis/jvm/exception

相关文章
|
JavaScript 前端开发 PHP
【Vue+NodeJS】vue路由及NodeJS环境搭建(Windows版)
Node.js是一个基于Chrome V8引擎构建的JavaScript运行时环境使用了一个事件驱动、非阻塞式I/O 的模型;可以让JavaScript在服务器端运行,它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。相对于传统的JavaScript运行环境(如浏览器),Node.js具有以下特点:事件驱动:Node.js使用事件驱动的非阻塞I/O模型,使得在处理大量并发请求时效率更高。单线程。
|
SQL 关系型数据库 MySQL
解决MySQL主从慢同步问题的常见的解决方案:
解决MySQL主从慢同步问题的方法有很多,以下是一些常见的解决方案: 1. 检查网络连接:确保主从服务器之间的网络连接稳定,避免网络延迟或丢包导致数据同步缓慢。 2. 优化数据库配置:调整MySQL的配置参数,如增大binlog文件大小、调整innodb_flush_log_at_trx_commit等参数,以提高主从同步性能。 3. 检查IO线程和SQL线程状态:通过SHOW SLAVE STATUS命令检查IO线程和SQL线程的状态,确保它们正常运行并没有出现错误。 4. 检查主从日志位置:确认主从服务器的binlog文件和位置是否正确,避免由于错误的日志位置导致同步延迟。 5.
1751 1
|
Linux 网络安全 Android开发
向日葵 Windows 端无法连接 Linux 问题的解决方案
向日葵 Windows 端无法连接 Linux 问题的解决方案
5422 0
|
数据采集 机器学习/深度学习 测试技术
数据清洗与过滤中,如何确定哪些数据是高质量的?
数据清洗与过滤中,如何确定哪些数据是高质量的?
|
9月前
|
存储 缓存 算法
JDK11升级后竟让内存利用率飙升到90%以上?
JDK11升级后竟让内存利用率飙升到90%以上?
|
机器学习/深度学习 数据采集 自然语言处理
【论文精读】大语言模型融合知识图谱的问答系统研究
论文题目:大语言模型融合知识图谱的问答系统研究
|
负载均衡 监控 Java
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
25331 7
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
|
算法 安全 Java
(七)JVM成神路之GC分代篇:分代GC器、CMS收集器及YoungGC、FullGC日志剖析
在《GC基础篇》中曾谈到过分代以及分区回收的概念,但基础篇更多的是建立在GC的一些算法理论上进行高谈阔论,而本篇则重点会对于分代收集器的实现进行全面详解,其中会涵盖串行收集器、并行收集器、三色标记、SATB算法、GC执行过程、并发标记、CMS收集器等知识,本篇则偏重于分析GC机制的落地实现,也就是垃圾收集器(Garbage Collector)。
795 8
|
SQL 关系型数据库 MySQL
MySQL数据库常见报错案例与错误代码说明
MySQL数据库常见报错案例与错误代码说明
1109 0
|
Java Maven 微服务
【工程】-一文带你使用Gradle构建SpringBoot微服务项目
【工程】-一文带你使用Gradle构建SpringBoot微服务项目
2294 0
【工程】-一文带你使用Gradle构建SpringBoot微服务项目