写在文章开头
你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:「写代码的SharkChili」,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。
之前写过一篇是「关于JVM方法区」的文章时,引发读者的探讨,有读者认为元空间实际上是可能触发垃圾回收,遂笔者就以这篇文章来探讨一下这个问题。

深入理解Java虚拟机的说法
笔者查阅权威 「《深入理解Java虚拟机》」 中看到,「《Java虚拟机规范》」 对于方法区的实现即元空间或者永久代垃圾回收行为没有强制要求。 原因很简单,方法区进行垃圾收集的回收的收益不是很大,它并不像堆内存的新生代那样,在一次新生代的垃圾回收就能回收 「70%-90%」 的内存空间。这也使得大部分人(包括笔者)认为方法区不涉及GC的,实际上对于 「jdk8」 版本的「Hotspot」虚拟机而言,「JVM」 中某一个类符合以下这3个条件时将会卸载类并回收这个类的元数据空间:
在堆中没有任何基于当前类或者基于该类派生子类的实例。 该类的「java.lang.Class」对象没有在任何地方被引用,以及无法通过反射等方式访问该类的方法。 加载该类的类加载器被回收,这个条件除非是精心设计过的可替换类加载器的场景,否者很难实现。所以元空间回收会因扫描堆内存伴随GC。
注意事项
可以看到最后一点比较苛刻,所以就导致如果我们使用「Spring」等框架通过增强技术生成大量的新类型载入元空间内存,导致元空间内存溢出 「(Caused by: java.lang.OutOfMemoryError: Metaspace)」 ,就像下面这段代码一样,为了更快看到效果,我们手动设置一下元空间大小-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
:
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
//设置代理目标
enhancer.setSuperclass(EmptyObject.class);
enhancer.setUseCache(false);
//设置单一回调对象,在调用中拦截对目标方法的调用
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(objects, args));
enhancer.create();
}
}复制
我们通过「jconsole」定位查看当前进程的类加载信息:

可以看到大量「EmptyObject」的增强类被加载至元空间中:

键入命令「jmap」 定位加载的类信息再次进行确认:
jmap -histo 4532
复制
可以看到生成了大量的「net.sf.cglib.proxy」相关的类
num #instances #bytes class name
----------------------------------------------
1: 3824742 600680704 [C
2: 1932145 170028760 java.lang.reflect.Method
3: 3806008 91344192 java.lang.String
4: 1779516 37754664 [Ljava.lang.Class;
5: 26568 15064520 [I
6: 618402 14841648 net.sf.cglib.core.Signature
7: 79344 12595728 java.lang.Class
8: 154765 12381200 java.lang.reflect.Constructor
9: 308844 9883008 net.sf.cglib.proxy.MethodProxy
10: 308844 9883008 net.sf.cglib.proxy.MethodProxy$CreateInfo复制
我们以「MethodProxy」进行定位可以看到这个类是在「create」方法创建的,这也就意味着上述代码的最后一个「create」方法会创建大量的「MethodProxy」并存到元空间中导致元空间内存溢出:
public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
proxy.sig1 = new Signature(name1, desc);
proxy.sig2 = new Signature(name2, desc);
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}复制
所以尽管说「jdk8」将类信息存到原空间中,但我们日常进行开发也需要留意对于「cglib」等增强技术的使用是否得当,如果发现大量的增强类出现在元空间时,需要及时定位并解决。
小结
我是「sharkchili」,「CSDN Java 领域博客专家」,「开源项目—JavaGuide contributor」,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号:「写代码的SharkChili」,同时我的公众号也有我精心整理的「并发编程」、「JVM」、「MySQL数据库」个人专栏导航。
参考
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 》