方法区溢出
# 方法区溢出
# 报错信息
java.lang.OutOfMemoryError: Metaspace
或 OutOfMemoryError:PermGen space
垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
JDK 7及以前,方法区使用永久代实现。JDK 8及后,元空间替换了永久代,元空间使用的是本地内存。
# 案例模拟
同一个Spring Boot应用,主要如下,详见案例代码。请求 http://localhost:8080/metaSpaceOom
/**
* 案例2:模拟元空间OOM溢出
*/
@RequestMapping("/metaSpaceOom")
public void metaSpaceOom(){
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(People.class);
// enhancer.setUseCache(false);
enhancer.setUseCache(true);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
System.out.println("我是加强类,输出print之前的加强方法");
return methodProxy.invokeSuper(o,objects);
});
People people = (People)enhancer.create();
people.print();
System.out.println(people.getClass());
System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
}
}
JVM参数配置:
-Xms80M
-Xmx80M
-XX:MetaspaceSize=80m
-XX:MaxMetaspaceSize=80m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof
-XX:SurvivorRatio=8
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-XX:+PrintGCDateStamps
-Xloggc:log/gc-oomMeta.log
PS:本人在测试的时候,短时间无法重现,因为发现-XX:MaxMetaspaceSize=80m
并没有起作用,通过VisualVM
发现元空间设置了1G的内存。建议使用main()
方法去测试,这里我直接使用老师的dump文件分析。
-XX:MetaspaceSize、-XX:MaxMetaspaceSize
关于这几个参数,发现某些实际场景与文档并不一致,建议阅读一下JVM参数MetaspaceSize的误解 (opens new window)
JVM参数配置:
我是加强类哦,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.People$$EnhancerByCGLIB$$6ef22046_10
totalClass:934
activeClass:934
unloadedClass:0
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
# 分析及解决
命令行查看查看GC状态:jstat GC 21052 1000 10
可以看到,Full GC 非常频繁,而且我们的方法区,占用了59190KB/1024 = 57.8M空间,几乎把整个方法区空间占用,所以得出的结论是方法区空间设置过小,或者存在大量由于反射生成的代理类。
VisualVM分析dump文件:
MAT分析dump文件:首先我们先确定是哪里的代码发生了问题,首先可以通过线程来确定,因为在实际生产环境中,有时候是无法确定是哪块代码引起的OOM,那么我们就需要先定位问题线程,然后定位代码,如下图所示。
定位到代码以后,发现有使用到cglib动态代理,那么我们猜想一下问题是不是由于产生了很多代理类,接下来,我们可以通过包看一下我们的类加载情况
这里发现Method类的实例非常多,查看with outging references ,这里发现了很多的People类在调用相关的方法:
所以问题就是出现在加载了很多代理类。
解决方案:
那么我们可以想一下解决方案,每次是不是可以只加载一个代理类即可, 因为我们的需求其实是没有必要如此加载的,当然如果业务上确实需要加载很多类的话,那么我们就要考虑增大方法区大小了 ,所以我们这里修改代码如下:
enhancer.setUseCache(true);
选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。 再看程序运行结果如下:
我是加强类哦,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.People$$EnhancerByCGLIB$$6ef22046
totalClass:6901
activeClass:6901
我是加强类哦,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.People$$EnhancerByCGLIB$$6ef22046
totalClass:6901
activeClass:6901+
可以看到,几乎不变了,方法区也没有溢出。到此,问题基本解决,再就是把while循环去掉。
常见原因与解决方法:
原因:
- 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
- 应用长时间运行,没有重启
- 元空间内存设置过小
解决方法:
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类