Java 异常机制Error
异常机制-Error
上一篇已经分析过Java异常类层次结构,以及异常机制-Exception详解,这篇主要写Throwable 的另一个子类:Error(错误)
Error(错误)
-
Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
-
Error错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如
OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。 -
Error错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!

在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。
根据多年项目经验总结六种生产项目中出现过的OOM场景——原因分析及解决方案
一、 StackOverflowError
JVM 运行时数据区中JVM 虚拟机栈是有深度的,在执行方法的时候会伴随着栈帧入栈和出栈,遵循“先进后出/后进先出”原则。如果方法执行后不停的递归,迟早把栈撑爆了
/**** 生产模拟代码**/
public class StackOverflowErrorDemo {public static void main(String[] args) {javaKeeper();}private static void javaKeeper() {javaKeeper();}
}Exception in thread "main" java.lang.StackOverflowErrorat oom.StackOverflowErrorDemo.javaKeeper(StackOverflowErrorDemo.java:15)
原因分析
- 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法,由于条件终止判断问题等而导致无法退出的情况
- 执行调用了大量方法,导致线程栈空间耗尽
- 方法内声明了海量的局部变量
- 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
解决方案
- 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug
- 排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常)
- 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制
二.、Java heap space
Java 堆用于存储对象实例,在 Java 堆中只要不断的创建对象,并且 GC-Roots 到对象之间存在引用链,这样 JVM 就不会回收对象,那随着对象数量的增加,总容量触及堆的最大容量限制后就会产生内存溢出异常。
/**** 示例1* 生产模拟代码**/
public static void main(String[] args) {List<String> list = new ArrayList<>(10) ;while (true){list.add("1") ;}}
当出现 OOM 时可以通过工具来分析 GC-Roots 引用链 (opens new window) ,查看对象和 GC-Roots 是如何进行关联的,是否存在对象的生命周期过长,或者是这些对象确实改存在的,那就要考虑将堆内存调大了。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3210)at java.util.Arrays.copyOf(Arrays.java:3181)at java.util.ArrayList.grow(ArrayList.java:261)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)at java.util.ArrayList.add(ArrayList.java:458)at com.crossoverjie.oom.HeapOOM.main(HeapOOM.java:18)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)Process finished with exit code 1
/*** 生产模拟代码:示例2* 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,分配内存就不够用,就类似于将 XXL 号的对象,往 S 号的 Java heap space 里面塞。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat oom.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:13)
原因分析
- 请求创建一个超大对象,通常是一个大数组
- 不断的创建对象,GC-Roots 到对象之间存在引用链
- 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
- 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
解决方案
- 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级
- 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
- 需要通过JVM 参数 -Xmx 参数调高 JVM 堆内存空间
内存泄露和内存溢出
内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个 Integer,但给它存了 Long 才能存下的数,那就是内存溢出。
内存泄露( memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
三、GC overhead limit exceeded
当时的生产现象是 CPU告警 使用率 100%,JVM 内置了垃圾回收机制GC,所以我们不需要手工编写代码来进行内存分配和释放,但是当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误(俗称:垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。
假如不抛出 GC overhead limit exceeded 错误,那 GC 清理的那么一丢丢内存很快就会被再次填满,迫使 GC 再次执行,这样恶性循环,CPU 使用率 100%。
/*** 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 < 10000; i++){if (!m.containsKey(new Key(i))){m.put(new Key(i), "num:" + 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
原因分析
从输出结果可以看到,我们的限制条件没有起作用,map 容量远超过了 10000,而且最后也出现了我们想要的错误,这是因为类 Key 只重写了 hashCode() 方法,却没有重写 equals() 方法,我们在使用 containsKey() 方法其实就出现了问题,当时生产环境也是终止条件出现问题,于是就会一直往HashMap 中添加 Key,直至 GC 都清理不掉。
执行这个程序的最终错误,和 JVM 配置也会有关系,如果设置的堆内存特别小,会直接报 Java heap space。算是被这个错误截胡了,所以有时,在资源受限的情况下,无法准确预测程序会死于哪种具体的原因。
解决方案
- 检查项目中是否有大量的死循环或有使用大内存的代码,比如示例中一直往HashMap 中添加数据
- dump内存分析,检查是否存在内存泄露,如果没有,加大内存
- 通过JVM 参数-Xmx,调高JVM 堆内存空间
四:Metaspace
JDK8取消永久代,类加载之后的类信息(类型信息、字段、方法、常量)保存在本地内存的元空间MetaSpace ,但字符串常量池、静态变量仍在堆中,,如果不指定该区域的大小,JVM 将会动态的调整。
/***生产代码示例:*可以使用 -XX:MaxMetaspaceSize=10M 来限制最大元数据*这样当不停的创建类时将会占满该区域并出现 OOM*/public static void main(String[] args) {while (true){Enhancer enhancer = new Enhancer() ;enhancer.setSuperclass(HeapOOM.class);enhancer.setUseCache(false) ;enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return methodProxy.invoke(o,objects) ;}});enhancer.create() ;}}使用 cglib 不停的创建新类,最终会抛出:Caused by: java.lang.reflect.InvocationTargetExceptionat sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)... 11 more
Caused by: java.lang.OutOfMemoryError: Metaspaceat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:763)... 16 more
这里的 OOM 伴随的是 java.lang.OutOfMemoryError: Metaspace 也就是元数据溢出。
解决方案:
- 检查项目中是否有生成大量动态类的应用场景中,应该特别关注这些类的回收情况。例如上边的 GCLib 字节码增强和动态语言
- -XX:MaxMetaspaceSize 设置元空间最大值,默认是 -1,表示不限制(还是要受本地内存大小限制的)
- -XX:MetaspaceSize 指定元空间的初始空间大小,以字节为单位,达到该值就会触发 GC 进行类型卸载,同时收集器会对该值进行调整
- -XX:MinMetaspaceFreeRatio 在 GC 之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集频率,类似的还有 MaxMetaspaceFreeRatio
五、Unable to create new native thread
每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
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
原因分析
- 线程数超过操作系统最大线程数限制(和平台有关)
- 线程数超过 kernel.pid_max(只能重启)
native 内存不足
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么 Java虚拟机将会抛出一个OutofMemoryError异常
解决方案
- 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程
- 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制
六、Direct buffer memory
我们使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。
/**
*1、ByteBuffer.allocate(capability) 是分配 JVM 堆内存,属于 GC 管辖范围,
*需要内存拷贝所以速度相对较慢;
*2、ByteBuffer.allocateDirect(capability) 是分配 OS 本地内存,不属于 GC 管辖范围,
*由于不需要内存拷贝所以速度相对较快;
*
*
*Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过
*Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
*//*** 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
如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。
解决方案
- Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过
Arthas 等在线诊断工具拦截该方法进行排查 - 检查是否直接或间接使用了 NIO,如 netty,jetty 等
- 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
- 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效
- 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
