谈谈你对OOM的理解

结论先行:常见的OOM

(一)栈内存溢出:java.lang.StackOverflowError

(二)堆内存溢出:java.lang.OutOfMemoryError: Java heap space

(三)直接内存溢出:java.lang.OutOfMemoryError: Direct buffer memory

(四)“垃圾回收上头”:java.lang.OutOfMemoryError: GC overhead limit exceeded

(五)元空间溢出:java.lang.OutOfMemoryError: Metaspace

(六)线程创建过多:java.lang.OutOfMemoryError: Unable to create new native thread

一. StackOverflowError

1.1 写个bug
/**
 * 栈空间溢出,属于错误!!!!
 */
public class StackOverflowErrorDemo {
    public static void main(String[] args) {
        StackOverflowError();
    }
    public static void StackOverflowError(){
        StackOverflowError();
    }
}
1.2 bug现象

在这里插入图片描述

1.3 记住,这属于错误分支,不是异常!

在这里插入图片描述

1.4 现象分析

  ♦ 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况;
  ♦ 执行了大量方法,导致线程栈空间耗尽;
  ♦ 方法内声明了海量的局部变量;

1.5 解决方案

  ♦ 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug;
  ♦ 排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常);
  ♦ 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制;

二. Java heap space

2.1 写个bug
public class JavaHeapSpaceDemo {
    public static void main(String[] args) {
       // byte[] bytes = new byte[30*2014*2014];
        String str = "javaheapsapce";
        while (true){
            // 故意生成很多新对象
            // 带上参数: -Xms10m  -Xmx10m
            str += str + new Random().nextInt(100000)+ new Random().nextInt(500000);
            str.intern();
        }
    }
}
2.2 bug现象

在这里插入图片描述

2.3 现象分析

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

2.4 解决方案

   针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:
  ♦ 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制;
  ♦ 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级;
  ♦ 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接;

三. Direct buffer memory

3.1 写个bug
import java.nio.ByteBuffer;
/**
 * 导致原因:
 *      写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲(Buffer)的I/O 方式,它可以使用Native函数库直接分配堆外内存,
 *      然后通过一个存储再Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能再一些场景中显著提高性能,因为避免了再java堆和native堆中来回复制数据。
 *
 *      ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
 *      ByteBuffer.allocteDirect(capability)第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
 *
 *      但是如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC, DirectByteBuffer对象他们就不会被回收,这时候堆内存充足,但本地内存可能已经使用
 *      完了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
 */
public class DirectBufferDemo {
    // -Xms10m  -Xmx10m  -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
    public static void main(String[] args) throws Exception {
        System.out.println("配置demaxDirectMemory"+(sun.misc.VM.maxDirectMemory()/(double)1024/1024)+"MB");
        Thread.sleep(3000);
        ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
    }
}
3.2 bug现象

在这里插入图片描述

3.3 现象分析

在这里插入图片描述

  ♦ Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误。

3.4 解决方案

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

四. GC overhead limit exceeded

4.1 写个bug
import java.util.ArrayList;
import java.util.List;
/**
 * GC回收时间过长时会抛出outOfMemroyError。过长的定义是,超过98%的时间用来做CC并且回收了不到2%的堆内存连续多校GC都只回收了到2%的极端情况下才会地出。假如不抛出GC overhead Limit错误会发生什么情况呢?
 * 那就是G清理的这么点内存很快会再次填满,迫使GC再次执行.这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果
 */
public class GCOverheadDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true){
                list.add(String.valueOf(++i).intern());
            }
        }catch (Throwable e){
            System.out.println("*** i="+i);
            e.printStackTrace();
            throw e;
        }
    }
}
4.2 bug现象

不知道为啥,我实现出来的一直显示不出来 GC overhead limit exceeded
在这里插入图片描述

4.3 现象分析

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

4.4 解决方案

  ♦ 添加 JVM 参数-XX:-UseGCOverheadLimit 不推荐这么干,没有真正解决问题,只是将异常推迟;
  ♦ 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码;
  ♦ dump内存分析,检查是否存在内存泄露,如果没有,加大内存;

五. Metaspace

5.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();
        }
    }
}
5.2 bug现象
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
5.3 现象分析

  ♦ JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。

5.4 解决方案

   方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了上边的 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。
  ♦ -XX:MaxMetaspaceSize 设置元空间最大值,默认是 -1,表示不限制(还是要受本地内存大小限制的)
  ♦ -XX:MetaspaceSize 指定元空间的初始空间大小,以字节为单位,达到该值就会触发 GC 进行类型卸载,同时收集器会对该值进行调整
  ♦ -XX:MinMetaspaceFreeRatio 在 GC 之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集频率,类似的还有 MaxMetaspaceFreeRatio

六. Unable to create new native thread

6.1 写个bug
public class UnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("******** i = "+i);
            new Thread(()->{
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}
6.2 bug现象

在这里插入图片描述

6.3 现象分析

   JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:
  ♦ 线程数超过操作系统最大线程数限制(和平台有关)
  ♦ 线程数超过 kernel.pid_max(只能重启)

6.4 解决方案

  ♦ 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程;
  ♦ 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值