2023-08-10  阅读(2)
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/97

一、简介

本章,我们将通过示例代码演示Metaspace区域是如何发生内存溢出的,并根据内存快照进行分析。

我们回顾下Metaspace区发生内存溢出的一个场景:程序不停的动态生成类,然后不停的加载类到Metaspace区域,而且这些动态生成的类必须得是不能被回收的,一旦Metaspace区满了,就会触发Full GC,而由于Metaspace区中的对象无法被回收,此时就触发了Metaspace内存溢出。

二、示例程序

我们的示例程序采用CGLIB来动态生成类。

2.1 程序源码

    public class Demo1 {
        public static void main(String[] args) {
            long count = 0L;
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(Car.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        if (method.getName().equals("run")) {
                            System.out.println("Before run, security checking...");
                            return methodProxy.invokeSuper(o, objects);
                        } else {
                            return methodProxy.invokeSuper(o, objects);
                        }
                    }
                });
    
                Car car = (Car) enhancer.create();
                car.run();
    
                System.out.println("Created " + ++count +" Car.");
            }
        }
    
        static class Car {
            public void run() {
                System.out.println("Car is running...");
            }
        }
    
        static class SafeCar extends Car{
            @Override
            public void run() {
                System.out.println("Car is running...");
                super.run();
            }
        }
    }

上述代码,通过CGLIB的Enhancer生成了一个Car的代理子类:

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Car.class);
    enhancer.setUseCache(false);

既然是Car的子类,就会有Car的所有方法,然后我们在调用子类的run方法时做了点手脚:

    enhancer.setCallback(new MethodInterceptor() {
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            if (method.getName().equals("run")) {
                System.out.println("Before run, security checking...");
                return methodProxy.invokeSuper(o, objects);
            } else {
                return methodProxy.invokeSuper(o, objects);
            }
        }
    });

上述这段代码的意思是:如果调用了Car子类对象的方法,会先被这里的MethodInterceptor拦截,拦截后判断如果是run方法,则先做一些额外的工作——汽车安全检查,最后再执行父类的run方法。效果等同于:

    static class SubCar extends Car {
        @Override
        public void run() {
            System.out.println("Before run, security checking...");
            super.run();
        }
    }

2.2 JVM参数

接着,我们需要通过JVM参数限制下Metaspace区域的大小,我们把它设置为10MB,然后开启内存溢出时自动dump内存快照:
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./

使用该参数执行程序,可以看到如下所示的打印输出,当创建到第258辆Car时,Metaspace区的内存被耗尽了,导致java.lang.OutOfMemoryError: Metaspace

    Created 258 Car.
    java.lang.OutOfMemoryError: Metaspace
    Dumping heap to ./\java_pid11836.hprof ...
    Heap dump file created [3470456 bytes in 0.120 secs]
    Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
        at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
        at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
        at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
        at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
        at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
        at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
        at com.ressmix.jvm.Demo1.main(Demo1.java:27)
    Caused by: java.lang.reflect.InvocationTargetException
        at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
        at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
        ... 6 more
    Caused by: java.lang.OutOfMemoryError: Metaspace
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
        ... 11 more

三、问题分析

上述程序执行完后,会在程序根目录生成两个文件:gc.logjava_pid11836.hprof,gc.log是JVM运行时信息,java_pid11836.hprof就是内存快照。

3.1 GC日志分析

我们先来分析下gc.log:

    1.218: [GC (Allocation Failure) 1.276: [ParNew: 52480K->2051K(59008K), 0.0160380 secs] 52480K->2051K(190080K), 0.0745051 secs] [Times: user=0.05 sys=0.00, real=0.08 secs] 
    1.631: [GC (Allocation Failure) 1.631: [ParNew: 54531K->2855K(59008K), 0.0025661 secs] 54531K->2855K(190080K), 0.0026505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    1.881: [Full GC (Metadata GC Threshold) 1.881: [CMS: 0K->2708K(131072K), 0.0464583 secs] 36239K->2708K(190080K), [Metaspace: 9885K->9885K(1058816K)], 0.0467198 secs] [Times: user=0.05 sys=0.01, real=0.05 secs] 
    1.927: [Full GC (Last ditch collection) 1.927: [CMS: 2708K->1749K(131072K), 0.0104116 secs] 2708K->1749K(190144K), [Metaspace: 9885K->9885K(1058816K)], 0.0104936 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
    2.012: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1749K(131072K)] 1749K(190144K), 0.0001810 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2.012: [CMS-concurrent-mark-start]
    2.022: [CMS-concurrent-mark: 0.011/0.011 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
    2.057: [CMS-concurrent-preclean-start]
    2.058: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     par new generation   total 59072K, used 1363K [0x0000000701a00000, 0x0000000705a10000, 0x00000007166c0000)
      eden space 52544K,   2% used [0x0000000701a00000, 0x0000000701b54c68, 0x0000000704d50000)
      from space 6528K,   0% used [0x0000000704d50000, 0x0000000704d50000, 0x00000007053b0000)
      to   space 6528K,   0% used [0x00000007053b0000, 0x00000007053b0000, 0x0000000705a10000)
     concurrent mark-sweep generation total 131072K, used 1749K [0x00000007166c0000, 0x000000071e6c0000, 0x00000007c0000000)
     Metaspace       used 9912K, capacity 10090K, committed 10240K, reserved 1058816K
      class space    used 890K, capacity 913K, committed 1024K, reserved 1048576K
    2.063: [GC (CMS Final Remark) [YG occupancy: 1363 K (59072 K)]2.063: [Rescan (parallel) , 0.0002642 secs]2.063: [weak refs processing, 0.0000124 secs]2.063: [class unloading, 0.0012829 secs]2.065: [scrub symbol table, 0.0005776 secs]2.065: [scrub string table, 0.0001698 secs][1 CMS-remark: 1749K(131072K)] 3112K(190144K), 0.0024292 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

先触发了两次Young GC,主要是因为不断创建Car对象,最终Eden区无法容纳。我们关键看下Full GC,第一次Full GC如下:

    1.881: [Full GC (Metadata GC Threshold) 1.881: [CMS: 0K->2708K(131072K), 0.0464583 secs] 36239K->2708K(190080K), [Metaspace: 9885K->9885K(1058816K)], 0.0467198 secs] [Times: user=0.05 sys=0.01, real=0.05 secs]

Metadata GC Threshold告诉我们是因为Metasapce区空间不足而引起Full GC。可以看到,Metasapce区的对象已经快占满了10MB了——[Metaspace: 9885K->9885K(1058816K)],经过这次Full GC,里面的对象并没有被回收掉,接着就进行下一次Full GC,这是最后的拯救机会(Last ditch collection):

    1.927: [Full GC (Last ditch collection) 1.927: [CMS: 2708K->1749K(131072K), 0.0104116 secs] 2708K->1749K(190144K), [Metaspace: 9885K->9885K(1058816K)], 0.0104936 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

结果还是一样,Metaspace区中的对象依旧无法回收,也不够容纳新创建的类对象,所以JVM直接终止运行,并打印出最后的堆内存的情况。

3.2 内存快照分析

我们通过MAT工具来对内存快照java_pid11836.hprof进行分析:

202308102133065341.png

可以看到,大量的AppClassLoader占用了内存,点击Details继续看,发现里面有一大堆Car$$EnhancerByCGLIB对象,正是因为CGLIB动态生成的这些类导致了Metaspace被占满:

202308102133073782.png

于是我们排查代码,看到底是哪里不断的动态创建类对象,发现Enhancer对象没有做缓存,所以只要加上缓存,不要无限制去生成类就可以了。

四、总结

本章,我们通过一个程序示例,不断利用CGLIB生成动态代理类的方式,模拟了Metaspace区内存溢出的场景。下一章,我们将模拟Java虚拟机栈内存溢出。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文