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

简介: 《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.StackOverflowError
 at oom.StackOverflowErrorDemo.javaKeeper(StackOverflowErrorDemo.java:15)

0.gif

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 space
 at oom.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:13)

2.2 原因分析

  • 请求创建一个超大对象,通常是一个大数组
  • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
  • 过度使用终结器(Finalizer),该对象没有立即被 GC
  • 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收

2.3 解决方案

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

  • 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
  • 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
  • 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接

97.jpg

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

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

内存泄露和内存溢出

内存溢出(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;
        }
        @Override
        public 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内存分析,检查是否存在内存泄露,如果没有,加大内存
相关文章
|
6月前
|
Java 数据库连接 容器
常见的几种内存泄漏情况和示例
常见的几种内存泄漏情况和示例
126 0
|
Java 开发者
使用jstack结合代码来演示【Java线程状态】
Java线程状态一直是让工程师容易迷惑的知识点,我觉得原因有二:一是线程的概念较为抽象,其状态转换的条件和时间点不容易理解;二是线程状态和进程状态不是完全对应的,且线程的状态词汇容易让人误解。下面我们通过jstack结合代码来探究一下Java线程状态相关的关键知识点。
317 0
|
6月前
|
编译器 C++
我终于体会到了:代码竟然不可以运行,为什么呢?代码竟然可以运行,为什么呢?
我终于体会到了:代码竟然不可以运行,为什么呢?代码竟然可以运行,为什么呢?
80 0
我终于体会到了:代码竟然不可以运行,为什么呢?代码竟然可以运行,为什么呢?
|
6月前
|
缓存 架构师 算法
Java内存溢出如何解决,Java oom排查方法,解决办法
在Java开发过程中,有效的内存管理是保证应用程序稳定性和性能的关键。不正确的内存使用可能导致内存泄露甚至是致命的OutOfMemoryError(OOM)。
|
6月前
|
缓存 架构师 算法
Java内存溢出如何解决,Java oom排查方法,10个定位解决办法
在Java开发过程中,有效的内存管理是保证应用程序稳定性和性能的关键。不正确的内存使用可能导致内存泄露甚至是致命的OutOfMemoryError(OOM)。
141 0
|
存储 Arthas 缓存
千万不要这样写代码!9种常见的OOM场景演示
在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。 本篇主要包括如下 OOM 的介绍和示例: java.lang.StackOverflowError java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: GC overhead limit exceeded java.lang.OutOfMemoryError--&gt;Metaspace
千万不要这样写代码!9种常见的OOM场景演示
|
数据可视化 Java Linux
OOM问题小例子及思考
前段时间朋友新进入一个公司,遇到线上系统cpu飙高,内存占满情况;和他交流过后有了本篇总结,该博文会以一个简单的小例子来演示oom,以及通过输出dump文件,最后用可视化工具进行分析定位具体oom的代码。
|
存储 Dart Java
Dart内存泄漏示例及如何解决
内存泄漏是指对象被分配了内存空间,但在不再需要这些对象时,它们仍然占用着内存空间而没有被垃圾回收。
Dart内存泄漏示例及如何解决
ThreadLocal内存溢出代码演示和原因分析!(2)
ThreadLocal内存溢出代码演示和原因分析!(2)
117 0
ThreadLocal内存溢出代码演示和原因分析!(2)
|
存储 Java
Java初学者作业——添加程序断点,以Debug模式运行程序,观察变量的交换
Java初学者作业——添加程序断点,以Debug模式运行程序,观察变量的交换
358 0
Java初学者作业——添加程序断点,以Debug模式运行程序,观察变量的交换