随着Java应用程序日益复杂和庞大,JVM(Java虚拟机)的性能优化变得尤为重要。在JVM的各种组件中,元空间(Metaspace)作为类元数据的存储区域,扮演着关键角色。本文将深入探讨JVM元空间的工作原理、架构设计及其对应用性能的影响,并提供实际的调优建议。通过本文,读者不仅能够全面了解元空间的基本概念,还能掌握如何有效管理和优化这一重要资源。
什么是JVM方法区
方法区主要是用于存储类信息、静态变量以及常量信息的。是各个线程共享的一个区域。我们都知道JVM中有个区域叫堆区,所以有时候人们也会称方法区为Non-Heap(非堆)。
在JDK8之前方法区存放在一个叫永久代的空间里。 在JDK8之后由于HotSpot 和JRockit 的合并,所以方法区就被作为元数据区了。
方法区和永久代是什么关系?
其实方法区并不是一个实际的区域,他不过是JVM虚拟机规范提出的一个概念而已。在HotSpot 实现方法区的方式就在JVM内存中划分一个区域作为永久代来存放这些数据。
在JDK8之前我们可以用下面的参数来调整永久代的大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
为什么JDK8之后要把永久代 (PermGen)换成元数据区(MetaSpace)
将数据放在永久代固然没问题,但是随着时间的推移,方法区使用的空间可能会逐渐变大,若我们分配大小不当很可能造成线上OOM问题,所以设计者们就在方法区移动到本地内存中,通过本地内存来存放数据。并且元数据区默认分配值为unlimited(我们也可以通过-XX:MetaspaceSize来动态调整),理论上是没有明确大小,是可以动态分配空间的,这样一来由于元数据区就不会受到JVM内存分配的约束了,所以理论上发生OOM的概率会小于永久代。
深入理解Java虚拟机关于方法区的说法
笔者查阅权威《深入理解Java虚拟机》 中看到,《Java虚拟机规范》 对于方法区的实现即元空间或者永久代垃圾回收行为没有强制要求。 原因很简单,方法区进行垃圾收集的回收的收益不是很大,它并不像堆内存的新生代那样,在一次新生代的垃圾回收就能回收70%-90% 的内存空间。这也使得大部分人(包括笔者)认为方法区不涉及GC的,实际上对于jdk8 版本的Hotspot虚拟机而言,JVM 中某一个类符合以下这3个条件时将会卸载类并回收这个类的元数据空间:
- 在堆中没有任何基于当前类或者基于该类派生子类的实例。
- 该类的java.lang.Class对象没有在任何地方被引用,以及无法通过反射等方式访问该类的方法。
- 加载该类的类加载器被回收,这个条件除非是精心设计过的可替换类加载器的场景,否者很难实现。
需要注意的是,在判断是否有实例还在使用当前类以及是否有类加载器引用这个类这两个步骤的时候,为了能够明确这两点,可能需要扫描全部堆空间的,这也就意味着元空间的回收可能伴随着FullGC。
代理对象创建不当导致元空间OOM问题
可以看到最后一点比较苛刻,所以就导致如果我们使用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等增强技术的使用是否得当,如果发现大量的增强类出现在元空间时,需要及时定位并解决。