
点击上方蓝字关注灸哥聊技术

点击上方蓝字关注灸哥聊技术

在开始这对 Java 中八种 OOM 错误类型的详细分析之前,有两个建议/提醒给到大家:
1.对 JVM 内存模型要做一个整体全面的复习,尤其各个区的深度原理和特性应用。
2.理论不经过实践永远都不会成为你的能力,针对这几天我文章的内容,各位看官一定要动手实践哦~
致所有程序员和运维人员
在面对 OOM 错误时,确实可以通用修改各个对应的参数来快速修复 OOM,但是请务必确认好你是推迟还是隐藏了 OOM 的症状。如果应用确实存在内存泄露或者确实加载了一些不合理的类,那你修改配置治标不治本,只是推迟了问题出现的时间罢了,并没有改善任何问题。
Java 应用程序在启动时会制定需要的内存大小,一般分割成两个不同的区域:
这两个区域在 JVM 启动时通过参数 -Xmx 和-XX:MaxPermSize 来设置,如果为主动设置,会使用特定平台的默认值。
当应用程序试图向堆空间添加更多数据,但堆已经没有足够的空间来容纳,就会触发该类型的 OOM,这里所说的足够的空间,是指堆空间设置的限制大小,而非实际的物理内存。
造成该类型 OOM 的常见原因一般有三种场景:
class OOM {
static final int SIZE = 2 * 1024 * 1024;
public static void main(String[] args) {
int[] arrays = new int[SIZE];
}
}
当应用程序使用超过 98% 的时间用来 GC 且回收不到 2% 堆内存时,就会抛出该类型的错误,具体表现就是几乎耗尽了所有可用内存,并且多次 GC 也不能清理干净。
这个错误是明确告诉你在 GC 上花费了太多时间但却没作用,如果没有这个限制,GC 进程会被重启,100% CPU 用于 GC 处理,就没有 CPU 空余资源去做其他事情了。
// 初始化一个 ma p并在无限循环中不停的添加键值对就会抛出该错误
public class Wrapper {
public static void main(String args[]) throws Exception {
Map map = System.getProperties();
Random r = new Random();
while (true) {
map.put(r.nextInt(), "value");
}
}
}
// 上述代码片段在启动时设置不同的堆空间大小或者不同的 GC 算法
java -Xmx10m -XX:+UseParallelGC Wrapper
// 看到错误
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Unknown Source)
at java.util.Hashtable.addEntry(Unknown Source)
at java.util.Hashtable.put(Unknown Source)
at cn.moondev.Wrapper.main(Wrapper.java:12)
// 但如果启动命令换成
java -Xmx100m -XX:+UseConcMarkSweepGC Wrapper
java -Xmx100m -XX:+UseG1GC Wrapper
// 结果
Exception: java.lang.OutOfMemoryError thrown from
the UncaughtExceptionHandler in thread "main"
// 错误已经被默认的异常处理程序捕获,并且没有任何错误的堆栈信息输出
Java 中堆空间是 JVM 管理的最大一块内存空间,在 JVM 启动时是可以指定堆空间的大小,然后堆会被分为两部分:
Young 区又分为三部分:
该类型错误是因为持久代所在区域的内存耗光。
持久代存储的主要是每个类的信息,诸如类加载器引用、运行时常量池、字段数据、方法数据、方法代码、方法字节码等。PermGen 的大小是取决于被加载类的数量和大小,太多的类或者太大的类被加载到持久代的时候,就会引发该类型的错误。
// 最简单
import javassist.ClassPool;
public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100_000_000; i++) {
generate("cn.moondev.User" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}
// 运行时请设置JVM参数:-XX:MaxPermSize=5m,值越小越好。
// 需要注意的是JDK8已经完全移除持久代空间,取而代之的是元空间(Metaspace),
// 所以示例最好的JDK1.7或者1.6下运行
// 代码在运行时不停的生成类并加载到持久代中,直到撑满持久代内存空间,
// 最后抛出java.lang.OutOfMemoryError:PermGen space。代码中类的生成使用了javassist库
OutOfMemoryError 错误在不同应用启动过程中发生时,对应的解决策略又是什么样子的呢?
如果是应用启动时触发该类型的错误,解决方案反而是最简单的,只需要增加 PermGen 区域的大小就行。
这一时期的该类型错误,需要对 dump 文件进行分析,首先找到引用在什么地方被持有,然后在应用中增加一个关闭的 hook。一般此时的问题原因来自于两个方面:
运行时的该类型错误发生时,要先检查 GC 是否允许你从 PermGen 区写在类,这是因为 JVM 的标准配置是比较保守的,只要类一旦创建,即使没有实例引用,依旧会保留在内存中,特别是需要动态创建大量的类,但是生命周期又不那么长的时候,允许 JVM 可以卸载类对应用是有很大益处的,配置参数的方式:-XX:+CMSClassUnloadingEnabled。
缺省情况下,该项配置未启用,一旦启用,GC 会对 PermGen 区进行扫描和清理不再使用的类。不过这个配置只在 UseConcMarkSweepGC 的场景生效,其他 GC 算法,这个配置是无效的,需要结合 -XX:+UseConcMarkSweepGC 配合使用。
如果此时你已经对 JVM 可以卸载类确保万无一失,但还是会出现内存溢出的话,就要继续对 dump 文件进行分析,通过工具继续寻找应该卸载但没有被卸载干净的类,然后对这个类进行排查,找到可疑对象,分析相关的代码,直到找到问题的根源。
根据 Java 内存模型,我们可以知道 PermGen 区用于存储类的名称、字段、方法以及字节码、常量池等,但从 Java8 之后,内存模型发生了巨大的变化:引入了 Metaspace 区,删除了 PermGen 区。
Java8 如此改变的原因:
Metaspace 大小取决于加载的类的数量以及对应声明的大小,所以引发该类型 OOM 的原因一般都是因为太多的类或者太大的类加载进 Metaspace 区。
public class Metaspace {
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) throws Exception{
for (int i = 0; ; i++) {
Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
System.out.println(i);
}
}
}
// 程序运行中不停的生成新类,所有的这些类的定义将被加载到Metaspace区,
// 直到空间被完全占用并且抛出java.lang.OutOfMemoryError:Metaspace。
// 当使用-XX:MaxMetaspaceSize = 32m启动时,大约加载30000多个类时就会死机
// 结果
31023
31024
Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
at javassist.ClassPool.toClass(ClassPool.java:1170)
at javassist.ClassPool.toClass(ClassPool.java:1113)
at javassist.ClassPool.toClass(ClassPool.java:1071)
at javassist.CtClass.toClass(CtClass.java:1275)
at cn.moondev.book.Metaspace.main(Metaspace.java:12)
.....
在遇到该类型的错误时,一般按照以下三个步骤进行排查:
解决该类型错误时,首先是最直接的方案,增加 Metaspace 区的大小,参数 -XX:MaxMetaspaceSize。
另外也可以从以下两个方面去解决和优化该类型错误:
JVM 中的线程运行是需要对应的空间的,如果有足够多的线程但是没有那么多空间时,就会触发该类型的错误,很明显就是你的应用达到了启动线程数量的极限了。
当 JVM 请求创建新线程时,但是系统却无法创建新的 Native 线程,就会 Unable to create new native thread 错误,一般出现改错误会经历一个过程:
1.JVM 中的应用请求创建新线程
2.JVM 向系统申请创建新的 Native 线程
3.系统尝试创建新的 Native 线程,对 Native 线程分配新的内存空间
4.因内存空间耗尽系统拒绝分配内存空间
while(true){
new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(10000000);
} catch(InterruptedException e) { }
}
}).start();
}
// 不停地创建并启动新的线程,当代码运行时,很快就达到OS的线程数限制,并抛出该错误
可以通过以下三个步骤基本可以确定该类型错误的原因:
最直接的解决方案就是在系统级别增加线程数限制来绕过它,当然治标不治本,如果你的应用可以产生一堆线程且出问题了,这时候很大概率是你的应用出了比较严重的代码错误。需要对线程处理相关的代码进行认真仔细的审查,然后针对性优化,尤其是线程使用方式的优化,比如使用线程池等。
Java 应用在启动时都会指定所需要的内存大小,通过 -Xmx 和其他启动参数指定。在 JVM 请求的总内存大于可用物理内存的情况下,操作系统会将内存中的数据交换到磁盘上去,Out of swap space 表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。
当应用向 JVM Native heap 请求分配内存失败并且 Native heap 也即将耗尽时,JVM 就会抛出 Out of swap space 错误。该错误消息中包含分配失败的大小和请求失败的原因。
Native Heap Memory 是 JVM 内部使用的 Memory,这部分的 Memory 可以通过 JDK 提供的 JNI 的去访问,这部分 Memory 效率很高,但是管理需要自己去做,如果没有把握最好不要使用,以防出现内存泄露问题。JVM 使用 Native Heap Memory 用来优化代码载入(JTI 代码生成),临时对象空间申请,以及JVM内部的一些操作。
这个问题往往发生在 Java 进程已经开始交换的情况下,现代的 GC 算法已经做得足够好了,当面临由于交换引起的延迟问题时,GC 暂停的时间往往会让大多数应用程序不能容忍。
java.lang.OutOfMemoryError:Out of swap space 一般是由操作系统级别的问题引起的:
还有可能是本地内存泄漏导致应用程序失败,比如应用调用了 Native code 连续分配内存,但却没有被释放。
最简单的方法还是增加系统的虚拟内存或交换空间,不过要注意不同的平台实现的方式是有差异的。
而对于这个问题的唯一真正可行有效的方式是升级机器可以有更多的内存以及优化自己的代码减少内存占用。
Java 对应用可以分配的最大数组大小是有限制的。不同平台限制有一定的差异,但通常在 1 到 21 亿个元素之间,遇到 Requested array size exceeds VM limit 错误时,就说明你的代码中有一个超大数组。
该错误由 JVM 中的 Native code 抛出。JVM 在为数组分配内存之前,会执行特定于平台的检查,分配的数据结构是否在此平台中是可寻址的。
首先就是确认你的代码中是否有请求创建超大数组,然后分析对应代码中是否有不合理的数组大小请求。
针对数组大小进行优化,避免创建过大的数组,当然也可以使用其他数据结构,比如 ByteBuffer。
操作系统是建立在进程的概念之上,进程是在内核中作业,其中有一个非常特殊的进程,名叫内存杀手(Out of memory killer)。当内核检测到系统内存不足时,OOM killer 就会被激活,然后选择一个进程杀掉。那哪一个进程会首当其冲呢?选择的算法和想法都很简单那就是谁占用内存最多,谁就被干掉。
当可用虚拟内存消耗到让整个操作系统面临风险时,就会产生 Out of memory:Kill process or sacrifice child 错误。在这种情况下,OOM Killer 会选择流氓进程并杀死它。
缺省情况下,Linux 内核允许进程请求比系统中可用内存更多的内存,但大多数进程实际上并没有使用完他们所分配的内存。Linux 内核采用的机制跟宽带运营商差不多,一般情况下都没有问题,但当大多数应用程序都消耗完自己的内存时,麻烦就来了,这些应用程序的内存需求加起来超出了物理内存的容量,内核就必须杀掉一些进程才能腾出空间保障系统正常运行。
public static void main(String[] args){
List<int[]> l = new java.util.ArrayList();
for (int i = 10000; i < 100000; i++) {
try {
l.add(new int[100000000]);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child
Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB
解决这个问题最直接有效的方法就是内存升级,当然也可以:
通过上述的排查方法和解决策略,可以有效地诊断和解决 Java 应用程序中的各种 OOM 问题,从而提升应用程序的稳定性和性能。开发者和运维人员应该熟悉这些方法和策略,以便在面对内存危机时能够迅速采取行动。




分享,收藏,点赞,在看一起来