38-Metaspace区域是如何因为类太多而发生内存溢出的?

简介: 永久代溢出,由于到了JDK8,已经完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替,因此后续我们不再说 方法区、永久代,直接使用Metspace元空间来称呼。

类加载进Metaspace

首先我们从Metaspace区域开始分析,因为之前我们就分析过了类的加载机制,我们的java代码都是通过编译后生成 .class字节码,然后由类加载器进行加载读取到我们的JVM内存中的,存放在Metaspace中。如下图所示:

因此如果加载的类很多,不停的加载进Metaspace,就会导致Metaspace 空间存满,从而发生OOM。

注意:

这里并不是说Metaspace空间存满了就直接发生OOM,而是一旦Metaspace区域满了,会先触发Full GC,连带着回收Metaspace里的类。

如果回收后并没有腾出多少空间,而JVM进程还在不断往里倒对象,那么才会引发OOM的问题。

这里的Metaspace回收条件其实是非常严苛的!比如这个类的类加载器先要被回收,这个类的所有对象实例都要被回收等,所以一旦Metaspace区域满了,未必能回收掉很多的类。

什么情况下会发生Metaspace内存溢出?

Metaspace区域一般很少发生内存溢出,如果发生了一般无非就以下两个原因:

  1. 很多程序员并不懂JVM的运行原理,在系统上线的时候也未设置过JVM参数,而是直接使用默认的JVM参数,导致Metaspace默认的大小才几十MB而已,很容易就不够。

我们可以通过如下两个参数进行Metaspace空间大小的设置:

   //指定元空间的初始空间大小, 以字节为单位, 达到该值就会触发垃圾收集进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX: MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。
   -XX:MetaspaceSize=512m  

   //设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存、大小。    
   -XX:MaxMetaspaceSize=512m 

   //作用是在垃圾收集之后控制最小的元空间剩余容量的百分比, 可减少因为元空间不足导致的垃圾收集的频率。 类似的还有-XX: Max-MetaspaceFreeRatio, 用于控制最大的元空间剩余容量的百分比。   
   -XX: MinMetaspaceFreeRatio:

这样我们的Metaspace区域的大小初始化以及最大的空间就是512MB。

JVM默认值依赖于平台,windows下,-XX:MetaspaceSize 是21M,-XX:MaxMetaspaceSize 是-1,即没有限制。 默认情况下,如果不指定大小,虚拟机可能会耗尽所有的可用系统内存,并且当Metaspace区域的大小达到 21M即会触发Full GC的发生并卸载没用的类,当然这是初始的高水位线,一旦触及这个水位线后,高水位线会进行重置,新的高水位线的值取决于GC后释放了多少Metaspace,如果释放的空间不足,那么在不超过 MaxMetaspaceSize时,会适当提高该值,如果释放空间过多,则适当降低该值。

  1. 当前的很多主流框架, 如Spring、 Hibernate对类进行增强时, 都会使用到CGLib这类字节码技术, 当增强的类越多, 就需要越大的方法区以保证动态生成的新类型可以载入内存 ,如果使用cglib过多导致生产的类没控制好过多后导致Metaspace区域满了,进而引发内存溢出。

方法区溢出也是一种常见的内存溢出异常, 一个类如果要被垃圾收集器回收, 要达成的条件是比较苛刻的。 在经常运行时生成大量动态类的应用场景里, 就应该特别关注这些类的回收状况。 这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外, 常见的还有: 大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类) 、 基于OSGi的应用(即使是同一个类文件, 被不同的加载器加载也会视为不同的类) 等

场景模拟:JVM Metaspace内存溢出

我们通过CGLIB来模拟动态生成来,演示Metaspace OOM溢出,首先通过Maven项目引入以下依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

然后模拟代码如下:

/**
 * JVM参数设置:
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class Demo {
   
   
    public static void main(String[] args) {
   
   
        while(true) {
   
   
            getOpenCoder();
        }
    }

    /**
     * 动态生成一个OpenCoder的子类并调用study方法
     */
    private static void getOpenCoder() {
   
   
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OpenCoder.class);
        enhancer.setUseCache(false);
        enhancer.setCallback(new MethodInterceptor() {
   
   
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
   
   
                if(method.getName().equals("study")){
   
   
                    System.out.println("关注OpenCoder公众号");
                    return methodProxy.invokeSuper(o,objects);
                }else{
   
   
                    return methodProxy.invokeSuper(o,objects);
                }
            }
        });

        OpenCoder openCoder = (OpenCoder) enhancer.create();
        openCoder.study();
    }
}

class OpenCoder{
   
   
    public void study(){
   
   
        System.out.println("study......");
    }
}

以上代码Demo类为运行类,OpenCoder类为需要被代理的类。

我们在Demo类中通过一个getOpenCoder()方法来生成一个OpenCoder类的代理类,并调用其对应的Study()方法,在每次调用该类的方法时都会被MethodInterceptor拦截,如果是OpenCoder的study方法,我们就先输出一句:关注OpenCoder公众号,然后再执行study方法,输出 study......

同时在main方法中,直接使用了while死循环来不断调用 getOpenCoder() 方法,由于我们设置了jvm参数,控制了Metaspace的大小为10M,应该程序很快就能导致Metaspace撑满,最终OOM异常,我们这里也可以加一个计数器,来统计到底创建了多少个OpenCoder代理类,如下所示:

    public static void main(String[] args) {
   
   
        int count = 0;
        while(true) {
   
   
            getOpenCoder();
            System.out.println("目前创建了"+ (++count)+"个OpenCoder类的代理类");
        }
    }

每调用一次 getOpenCoder() 方法我们就打印一次,最终程序异常停止后打印的最后一次就代表生成的个数,如下图所示:

可以发现在创建了245个OpenCoder的代理类后,10M的Metaspace区域就被耗尽了。而且异常信息描述也就是我们看到的 OutOfMemoryEoor: Metaspace区域。

图解Metaspace OOM

Demo类运行后的流程为:

  1. 类加载进Metaspace
  2. JVM进程调用Main方法
  3. Main栈帧入栈虚拟机栈
  4. Main中调用getOpenCoder()方法
  5. getOpenCoder()方法进栈,代码中用到OpenCoder类加载进Metaspace
  6. 动态生成代理类,加载进Metaspace
  7. 调用代理类的Study()方法
  8. Study()栈帧弹栈
  9. getOpenCoder()栈帧弹栈
  10. main栈帧中继续循环 4~7步骤

最后直到Metaspace区域存放不下导致内存溢出!

Metaspace内存溢出小结

ok,以上主要就是分析了下Metaspace区域发生内存溢出的原理,同时给出了两种常见的触发Metaspace内存溢出的场景。以及通过模拟CGLIB动态生成类的方式演示了Metaspace区域的内存溢出,以及内存原理流程。

大家以后只要记得,合理分配Metaspace区域(一般分配为512M即可),同时避免无限制的动态生成类,一般这块区域都是比较安全的,不至于触发内存溢出。

目录
相关文章
|
1月前
|
存储 C++
|
24天前
|
消息中间件 存储 测试技术
【消息队列开发】 实现MemoryDataCenterTests类——测试管理内存数据
【消息队列开发】 实现MemoryDataCenterTests类——测试管理内存数据
|
1月前
|
存储 C++
webserver--Buffer类实现内存缓冲区读写(全代码)
webserver--Buffer类实现内存缓冲区读写(全代码)
|
1月前
|
Java iOS开发 MacOS
8 种 Java- 内存溢出之四 -Metaspace
8 种 Java- 内存溢出之四 -Metaspace
|
24天前
|
消息中间件 存储 安全
【消息队列开发】 实现MemoryDataCenter类——管理内存数据
【消息队列开发】 实现MemoryDataCenter类——管理内存数据
|
14天前
|
程序员 C++
c++primer plus 6 读书笔记 第十二章 类和动态内存分配
c++primer plus 6 读书笔记 第十二章 类和动态内存分配
|
1月前
|
存储 编译器 C++
【C++】内存管理和模板基础(new、delete、类及函数模板)
【C++】内存管理和模板基础(new、delete、类及函数模板)
34 1
|
1月前
|
存储 编译器 程序员
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
119 0
|
1月前
|
编译器 C++
VS2022查看类内存布局
先右键点击属性, 选择左侧的C/C++==>命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局。切切注意, Layout跟指定的结构/类名CTest之间没有空格, 有空格就不对了. 这会只输出指定的结构的内存布局.这个开关输出所有类, 主要是一大堆编译器内部的结构的内存布局, 其实还有一个开关是。
82 0
|
1月前
|
安全 IDE Java
jvm性能调优实战 - 44Metaspace区域是如何因为类太多而发生内存溢出OOM
jvm性能调优实战 - 44Metaspace区域是如何因为类太多而发生内存溢出OOM
226 0