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

一. StackOverflowError

1.1 写个 bug

public class StackOverflowErrorDemo {public static void main(String[] args) {javaKeeper();}private static void javaKeeper() {javaKeeper();}
}

JVM 虚拟机栈是有深度的,在执行方法的时候会伴随着入栈和出栈,上边的方法可以看到,main 方法执行后不停的递归,迟早把栈撑爆了

Exception in thread "main" java.lang.StackOverflowErrorat oom.StackOverflowErrorDemo.javaKeeper(StackOverflowErrorDemo.java:15)

1.2 原因分析

  • 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况

  • 执行了大量方法,导致线程栈空间耗尽

  • 方法内声明了海量的局部变量

  • native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)

1.3 解决方案

  • 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug

  • 排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常)

  • 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制

二. Java heap space

Java 堆用于存储对象实例,我们只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 GC 清除这些对象,那随着对象数量的增加,总容量触及堆的最大容量限制后就会产生内存溢出异常。

Java 堆内存的 OOM 异常是实际应用中最常见的内存溢出异常。

2.1 写个 bug

/*** JVM参数:-Xmx12m*/
public class JavaHeapSpaceDemo {static final int SIZE = 2 * 1024 * 1024;public static void main(String[] a) {int[] i = new int[SIZE];}
}

代码试图分配容量为 2M 的 int 数组,如果指定启动参数 -Xmx12m,分配内存就不够用,就类似于将 XXXL 号的对象,往 S 号的 Java heap space 里面塞。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat oom.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:13)

2.2 原因分析

  • 请求创建一个超大对象,通常是一个大数组

  • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值

  • 过度使用终结器(Finalizer),该对象没有立即被 GC

  • 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收

2.3 解决方案

针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:

  • 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制

  • 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。

  • 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接

面试官:说说内存泄露和内存溢出

加送个知识点,三连的终将成为大神~~

内存泄露和内存溢出

内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个 Integer,但给它存了 Long 才能存下的数,那就是内存溢出。

内存泄露( memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak 最终会导致 out of memory!

三、GC overhead limit exceeded

JVM 内置了垃圾回收机制GC,所以作为 Javaer 的我们不需要手工编写代码来进行内存分配和释放,但是当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误(俗称:垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。

假如不抛出 GC overhead limit exceeded 错误,那 GC 清理的那么一丢丢内存很快就会被再次填满,迫使 GC 再次执行,这样恶性循环,CPU 使用率 100%,而 GC 没什么效果。

3.1 写个 bug

出现这个错误的实例,其实我们写个无限循环,往 List 或 Map 加数据就会一直 Full GC,直到扛不住,这里用一个不容易发现的栗子。我们往 map 中添加 1000 个元素。

/*** JVM 参数: -Xmx14m -XX:+PrintGCDetails*/
public class KeylessEntry {static class Key {Integer id;Key(Integer id) {this.id = id;}@Overridepublic int hashCode() {return id.hashCode();}}public static void main(String[] args) {Map m = new HashMap();while (true){for (int i = 0; i < 1000; i++){if (!m.containsKey(new Key(i))){m.put(new Key(i), "Number:" + i);}}System.out.println("m.size()=" + m.size());}}
}
...
m.size()=54000
m.size()=55000
m.size()=56000
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

从输出结果可以看到,我们的限制 1000 条数据没有起作用,map 容量远超过了 1000,而且最后也出现了我们想要的错误,这是因为类 Key 只重写了 hashCode() 方法,却没有重写 equals() 方法,我们在使用 containsKey() 方法其实就出现了问题,于是就会一直往 HashMap 中添加 Key,直至 GC 都清理不掉。

????????‍???? 面试官又来了:说一下HashMap原理以及为什么需要同时实现equals和hashcode

执行这个程序的最终错误,和 JVM 配置也会有关系,如果设置的堆内存特别小,会直接报 Java heap space。算是被这个错误截胡了,所以有时,在资源受限的情况下,无法准确预测程序会死于哪种具体的原因。

3.2 解决方案

  • 添加 JVM 参数-XX:-UseGCOverheadLimit 不推荐这么干,没有真正解决问题,只是将异常推迟

  • 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码

  • dump内存分析,检查是否存在内存泄露,如果没有,加大内存

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

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 请求的总内存大于可用物理内存的情况下,操作系统开始将内容从内存换出到硬盘驱动器。

该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(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


往期推荐

这8种常见的SQL错误用法,你还在用吗?

2020-11-27

用好MySQL的21个好习惯!

2020-11-25

这么简单的三目运算符,竟然这么多坑?

2020-11-24

关注我,每天陪你进步一点点!

千万不要这样写代码!9种常见的OOM场景演示相关推荐

  1. python手写代码面试_常见Python面试题—手写代码系列

    原标题:常见Python面试题-手写代码系列 1.如何反向迭代一个序列 #如果是一个list,最快的方法使用reverse tempList = [1,2,3,4] tempList.reverse( ...

  2. python手写代码面试_常见Python面试题 — 手写代码系列

    原标题:常见Python面试题 - 手写代码系列 作者: Peace & Love 来自:https://blog.csdn.net/u013205877/article/details/77 ...

  3. 缓存的6种常见的使用场景

    1.1.作为数据库的缓存,为数据库减压 通常情况下,数据是存储在数据库的,应用程序也是直接操作数据库.在访问量较小的时候几乎没有什么影响. 一旦读写请求量超过1w,数据库压力剧增,此时可以从数据库角度 ...

  4. es elasticsearch 几种常见查询场景 二次分组 java读取es的查询json文件

    大家好,我是烤鸭: es中几种常见的查询场景,使用java读取es的json文件进行查询. es 中文使用手册. https://www.elastic.co/guide/cn/elasticsear ...

  5. Top K问题系列之三 手写代码

    Top K问题是面试时手写代码的常考题,某些场景下的解法与堆排和快排的关系紧密,所以把它放在堆排后面讲. 关于Top K问题最全的分类总结是在这里(包括海量数据的处理),个人将这些题分成了两类:一类是 ...

  6. 会写代码的AI开源了!C语言写得比Codex还要好,掌握12种编程语言丨CMU

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 萧箫 发自 凹非寺 量子位 | 公众号 QbitAI 比Codex还 ...

  7. Java几种常见排序算法与代码实现

    前言: 排序算法也算是每年校招.春招.社招都会问到的问题,虽然每次复习了就忘,但是也可以隔一段时间又拿出来看看. 其中,排序方式指,内部还是外部排序.只需要内部内存就可以的称为内部排序,数据量太大需要 ...

  8. 除了写代码,程序员必备的6种软技能

    程序员除了掌握日常编程写代码的技能,还需要加强哪些编程以外的软技能呢? 职业心态 主要是从业心态,对待工作的心态,要把工作当作自己的作品,用创业的心态去打工,这是一种优秀的职业素养.拿了工资,就要体现 ...

  9. 写代码写到70岁,是一种什么样的体验?

    点击上方 前端Q,关注公众号 回复加群,加入前端Q技术交流群 你听说过年纪最大的程序员,今年多少岁了? 鹅厂马化腾,1971年出生,今年50岁 小米ceo雷军,1969年出生,今年52岁 金山软件创始 ...

最新文章

  1. 开发购物车应用程序(1)
  2. CentOS 7.2 搭建内网ntp时间服务器
  3. rtc关机闹钟6 AlarmManagerService研究
  4. 关于 Deployer 部署结构
  5. 你好,脂肪肝!来自22岁的大学生的体检报告
  6. 一文看尽 CVPR2022 最新 22 篇论文(附打包下载)
  7. 扩展 GRTN:云原生趋势下的 RTC 架构演进
  8. Processing绘制四边形
  9. php拖拽原理,JS拖拽效果及原理解析
  10. 62 getproperty对象
  11. java 消费者模式 多线程_[Java并发-24-并发设计模式] 生产者-消费者模式,并发提高效率...
  12. 文本分类(一)封装分词器
  13. oracle 移动数据文件(装)
  14. Java(31)_JDBC连接mysql数据库(一)
  15. 汇编程序基本结构+利用DOS系统编译连接汇编程序
  16. 方法论 | 多因子策略的五大讨论(思维导图收藏版)
  17. 练手python_在图像上增加数字
  18. [4K测试视频] 杜比4K UHD蓝光演示碟 Dolby.UHD.BluRay.Demo.Disc.March.2018.2160p.BluRay下载
  19. VS Code常用快捷键
  20. 【Web】CSS(No.33)Css页面布局经典案例(三)《京东首页》

热门文章

  1. centos linux7修改主机名,CentOS7操作系统下永久修改主机名
  2. 尤克里里怎么样_尤克里里和吉他区别?尤克里里与吉他相比有什么不可替代的优势...
  3. pytorch:固定部分层参数,固定单个模型
  4. oracle中修改process
  5. 《Android 应用测试指南》——第2章,第2.4节包浏览器
  6. HDU 5145 - NPY and girls
  7. CentOS7安装Hadoop2.7完整流程
  8. 融于心而表于行 之 程序的执行问题
  9. Apache/Tomcat/JBOSS/Nginx(2)
  10. linux下VI模式中上下左右键和回退键出现字母