OOM简介及模拟场景

OOM,Out Of Memory,内存溢出,发生内存溢出是会导致JVM挂掉的。

在我们启动JVM的时候,指定了一些内存参数,JVM内存大小是有限的,如果不停地往里面放东西,那么在一定情况下就会发生OOM。

那我们具体来看一看哪些区域会发生OOM,以及分别在什么条件下会发生OOM。

JVM内存区域可以划分为:方法区(元数据区),堆内存,虚拟机栈,本地方法栈,程序计数器。

程序计数器:
其中程序计数器,是每个线程私有,而且就算保存一个字节码执行的地址,不存在内存溢出的情况。

虚拟机栈:
虚拟机栈也是线程私有,里面存放的是一个一个栈帧,对应一个又一个方法的执行,出栈之后自动消失,也不存在内存溢出的情况,但是当无限入栈的时候,会导致栈溢出:Stack Over Flow。一般每个线程的栈大小为500KB,或1MB,一个栈帧大小为1KB,那么除非一个线程在执行过程中调用了500个方法,或是1000个方法,但正常代码的调用链绝不可能这么深,更通常的导致Stack Over Flow的原因在于方法自调用:

public void test(){

System.out.println(“test here”);

test();

}

像这样,就会导致执行到test()方法的线程无限入栈,最终导致Stack Over Flow。

元数据区:
元数据区里面存放了JVM运行过程中通过类加载器加载进去的所有类。当元数据区满了的时候会触发Full GC,Full GC的时候会回收元数据区里面的类,但是要回收一个类,条件失非常苛刻的:这个类的类加载器要先被回收,这个类的所有实例对象都要先被回收等等。如果Full GC之后元数据区还是满的,此时继续往里面放新的东西,则会触发OOM。

元数据区发生OOM的原因在于两个:

第一种原因:很多工程师不懂JVM的运行原理,没有设置元数据区大小,在上线系统的时候直接使用了默认的元数据区大小,但是对于一些大型系统来说,自身有很多的类要加载,以及依赖了较多的jar包,jar包中的类也要加载,最终导致,元数据区容纳不下需要加载的类,导致OOM。

第二种原因:很多人写代码的时候,会通过cglib之类的技术动态生成一个代理类,一旦代码没有控制好,那么就可能导致生成的类太多,本来只需要生成一个类对象即可,结果每次请求都需要生成一个类,最终导致元数据区里面放了太多cglib动态生成的类,导致元数据区OOM。

堆内存:
堆内存存放了所有创建的对象,而堆内存划分为了新生代和老年代,新生代在 Young GC之后,即使Survivor区放不下,也还是有老年代来提供空间担保,所以新生代是不会发生OOM。但是老年代在进行了Full GC之后,如果还是没有足够的空间来存放Young GC之后的对象,则会发生堆内存的OOM。

老年代发生OOM的原因:

第一个原因:系统承载的并发量太大,导致大量请求还处于正在处理中的状态,大量对象的引用还没有失去,大量对象存活,Young GC后放入老年代,Full GC后还是没有回收多少,继续Young GC后放入老年代,老年代OOM。

第二个原因:系统有内存泄漏的问题。在java中的内存泄漏指的是:一些对象本来已经不再会被使用到了,但是这样的对象还是有引用。导致GC时无法对这些对象进行回收,从而导致OOM。

 

OOM模拟场景

通过上文的分析,我们知道:只有可能在方法区(元数据区)和老年代中可能发生OOM,所以我们在这里模拟一下这两个发生OOM的场景。

元数据区发生OOM:
这里我们通过cglib来不断创建动态生成类的方式来模拟元数据区发生OOM的场景。

所谓的动态生成类,就是说这些类不是通过手写出来的。我们手写出来的类都是.java,然后会编译为.class,然后会被类加载器加载到元数据区成为静态生成的类。而动态生成的类是系统在运行的过程中,通过操作、编辑字节码从而动态生成的类。

启动JVM的时候设置元数据区大小:

-XX:MetaspaceSize=10m

-XX:MaxMetaspaceSize=10m

然后写以下代码:

public class MetaspaceOOMDemo {

public static void main(String[] args) {
int count = 0;
while (true){
System.out.println("count: " + ++count);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Car.class);//要动态生成一个Car类的子类
enhancer.setUseCache(false);//是否对动态类进行复用
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if(method.getName().equals("run")){
System.out.println("汽车启动之前的安全检查");
return proxy.invokeSuper(obj,args);//具体去调用Car类方法的地方
}else {
return proxy.invokeSuper(obj,args);
}
}
});
Car car = (Car) enhancer.create();//这里通过enhancer生成的car就是一个继承Car类的动态类生成的对象
car.run();
}
}
}

class Car{
public void run(){
System.out.println("汽车启动,开始行使...");
}
}

 

以上代码中因为设置了enhancer.setUseCache(false),所以会不断的创建新的动态类,这些动态类会不断地进入到元数据区中,最终导致元数据区OOM。

启动,发现跑到底246次的时候,发生:java.lang.OutOfMemoryError: Metaspace。这里可以看到是元数据区发生的OOM,而且程序直接退出了。

老年代发生OOM:
我们通过内存泄漏的方式来模拟老年代发生OOM。

设置JVM启动参数,将堆内存设置为1MB大小:

-Xms1m –Xmx1m

编写以下代码:

public class HeapOOMDemo {

public static void main(String[] args) {
long count = 0;
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
System.out.println("count: " + ++count);
}
}
}

 


以上代码会不断地创建Object对象,而且不会被释放,最终会因为内存泄漏问题,导致堆内存OOM。

启动,跑到第14053次的时候,发生:java.lang.OutOfMemoryError: Java heap space。堆内存溢出,而且程序直接退出了。

posted @ 2023-02-27 11:07  木木林2022  阅读(348)  评论(0)    收藏  举报