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月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
87 4
|
1月前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
29 3
|
1月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
1月前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
4月前
|
存储 程序员 Python
Python类的定义_类和对象的关系_对象的内存模型
通过类的定义来创建对象,我们可以应用面向对象编程(OOP)的原则,例如封装、继承和多态,这些原则帮助程序员构建可复用的代码和模块化的系统。Python语言支持这样的OOP特性,使其成为强大而灵活的编程语言,适用于各种软件开发项目。
45 1
|
4月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
4月前
|
缓存 程序员
封装一个给 .NET Framework 用的内存缓存帮助类
封装一个给 .NET Framework 用的内存缓存帮助类