类加载进Metaspace
首先我们从Metaspace区域开始分析,因为之前我们就分析过了类的加载机制,我们的java代码都是通过编译后生成 .class字节码,然后由类加载器进行加载读取到我们的JVM内存中的,存放在Metaspace中。如下图所示:
因此如果加载的类很多,不停的加载进Metaspace,就会导致Metaspace 空间存满,从而发生OOM。
注意:
这里并不是说Metaspace空间存满了就直接发生OOM,而是一旦Metaspace区域满了,会先触发Full GC,连带着回收Metaspace里的类。
如果回收后并没有腾出多少空间,而JVM进程还在不断往里倒对象,那么才会引发OOM的问题。
这里的Metaspace回收条件其实是非常严苛的!比如这个类的类加载器先要被回收,这个类的所有对象实例都要被回收等,所以一旦Metaspace区域满了,未必能回收掉很多的类。
什么情况下会发生Metaspace内存溢出?
Metaspace区域一般很少发生内存溢出,如果发生了一般无非就以下两个原因:
- 很多程序员并不懂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时,会适当提高该值,如果释放空间过多,则适当降低该值。
- 当前的很多主流框架, 如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类运行后的流程为:
- 类加载进Metaspace
- JVM进程调用Main方法
- Main栈帧入栈虚拟机栈
- Main中调用getOpenCoder()方法
- getOpenCoder()方法进栈,代码中用到OpenCoder类加载进Metaspace
- 动态生成代理类,加载进Metaspace
- 调用代理类的Study()方法
- Study()栈帧弹栈
- getOpenCoder()栈帧弹栈
- main栈帧中继续循环 4~7步骤
最后直到Metaspace区域存放不下导致内存溢出!
Metaspace内存溢出小结
ok,以上主要就是分析了下Metaspace区域发生内存溢出的原理,同时给出了两种常见的触发Metaspace内存溢出的场景。以及通过模拟CGLIB动态生成类的方式演示了Metaspace区域的内存溢出,以及内存原理流程。
大家以后只要记得,合理分配Metaspace区域(一般分配为512M即可),同时避免无限制的动态生成类,一般这块区域都是比较安全的,不至于触发内存溢出。