jvm性能调优实战 - 44Metaspace区域是如何因为类太多而发生内存溢出OOM

简介: jvm性能调优实战 - 44Metaspace区域是如何因为类太多而发生内存溢出OOM

Pre

上一篇文章我们已经把JVM的运行原理重新回顾了一遍,大家脑子里应该能重新浮现出JVM运行我们写的那些代码的流程图了。

而且结合那个运行流程,我们也点出来了有哪几块区域可能会发生内存溢出,那么今天我们就来着重分析一下Metaspace区域到底为什么会发生内存溢出。


Metaspace区域是如何触发内存溢出的?

在启动一个JVM时是可以设置很多参数的,其中有一些参数就是专门用来设置Metaspace区域的内存大小的,

如下两个参数就是用来设置Metaspace区域大小的:

-XX:MetaspaceSize=512m 
-XX:MaxMetaspaceSize=512m

我们看下图,图中我们就限定了Metaspace区域的内存大小为512m。

所以实际上来说,在一个JVM中,Metaspace区域的大小是固定的,比如512MB。

那么一旦JVM不停地加载类,加载了很多很多的类,然后Metaspace区域放满了,此时会如何?大家看下图。

大家如果还记得之前我们说过的频繁Full GC触发的几个问题,其中之一就是Metaspace区域满就会触发Full GC,Full GC会带着一块进行Old GC就是回收老年代的,也会带着回收年轻代的Young GC。

当然,Full GC的时候,必然会尝试回收Metaspace区域中的类,如下图所示。

所以一旦Metaspace区域满了,此时会触发Full GC,连带着回收Metaspace里的类。

那么什么样的类才是可以被回收的呢?

这个条件是相当的苛刻,包括不限于以下一些:比如这个类的类加载器先要被回收,比如这个类的所有对象实例都要被回收,等等。

所以一旦你的Metaspace区域满了,未必能回收掉里面很多的类。

那么一旦回收不了多少类,此时你的JVM还在拼命的加载类放到Metaspace里去,你觉得此时会发生什么事情?

显而易见,一旦你尝试回收了Metaspace中的类之后发现还是没能腾出来太多空间,此时还要继续往Metaspace中塞入更多的类,直接就会引发内存溢出的问题。因为此时Metaspace区域的内存空间不够了。

一旦发生了内存溢出就说明JVM已经没办法继续运行下去了,此时可能你的系统就直接崩溃了,这就是Metaspace区域发生内存溢出的一个根本的原理。


到底什么情况下会发生Metaspace内存溢出?

平心而论,Metaspace这块区域一般很少发生内存溢出,如果发生内存溢出一般都是因为两个原因:

  • 第一种原因,很多工程师他不懂JVM的运行原理,在上线系统的时候对Metaspace区域直接用默认的参数,即根本不设置其大小
    这会导致默认的Metaspace区域可能才几十MB而已,此时对于一个稍微大型一点的系统,因为他自己有很多类,还依赖了很多外部的jar包有有很多的类,几十MB的Metaspace很容易就不够了
  • 第二种原因,就是很多人写系统的时候会用cglib之类的技术动态生成一些类,一旦代码中没有控制好,导致你生成的类过于多的时候,就很容易把Metaspace给塞满,进而引发内存溢出

对于第一种问题,通常来说,有经验的工程师上线系统往往会设置对应的Metaspace大小,推荐的值在512MB那样,一般都是足够的。

对于第二种问题,我们用模拟代码给大家演示那种不停的生成大量的类的情况,让大家亲眼看到这种情况下是如何触发Metaspace内存溢出的。


Metaspace OOM 模拟

首先,我们来回顾一下之前一直用的一张图片,大家看下图:

在这张图片里,我们通过之前的讲解,可以清晰的看到JVM整体的运行原理,包括类加载,线程执行方法,虚拟机栈,堆内存创建对象,GC以及对象转移老年代,等等。而且在这个图里,我们也清晰标志出来了哪些环节可能会发生内存溢出。

因此在这里我们来回顾一下,Metaspace区域发生内存溢出的一个场景,说白了就是如果我们在程序里不停的动态生成类,就会导致不停的加载类到Metaspace区域里去,而且这些动态生成的类必须还得是不能被回收掉的。

接着一旦Metaspace区域满了,就会触发Full GC连带着回收Metaspace中的类,但是此时大量的类是不能被回收的。

因此即使触发过Full GC过后,Metaspace区域几乎还是不能放下任何一个类,此时必然会触发Metaspace区域的内存溢出,导致JVM也是崩溃掉,无法继续运行了。

CGLIB动态生成类的代码示例

<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.3.0</version>
</dependency>

接着我们就可以使用CGLIB来动态生成类了,大家看下面的代码:

首先我们可以看到我们在这里定义了一个类,代表了一个汽车,他有一个run()方法,执行的时候就会启动汽车,开始让汽车行驶,大家看下面的这个代码片段:

那么我们接着来看下面的代码片段,我们通过CGLIB的Enhancer类生成了一个Car类的子类

注意,从这里开始,就是开始动态生成类了,大家要仔细看,看下面的代码片段:

你权且当做Enhancer是用来生成类的一个API吧,看到片段里我们给Enhancer设置了一个SuperClass没有?这里的意思就是说Enhancer生成的类是Car类的子类,Car类是生成类的父类。至于那个UseCache是什么意思,就先别管了。

既然Enhancer动态生成的类是Car的子类,那么是不是Car有的方法子类都有?所以子类是不是也有Car的run()方法?

答案是肯定的,但我们现在想要在调用子类的run()方法的时候做点手脚,如下面代码片段:

这个片段的意思是:如果你调用子类对象的run()方法,会先被这里的MethodInterceptor拦截一下,拦截之后,各位看里面的代码,是不是判断了一下,如果你调用的Method是run方法,那么就先对汽车做一下安全检查。

安全检查做完之后,再通过“methodProxy.invokeSuper(o, objects);”调用父类Car的run()方法,去启动汽车,这行代码就会执行到Car类的run()方法里去了。

到此为止,我们就已经通过CGLIB的Enhancer生成了一个Car类的子类了,而且定义好了对这个子类调用继承自父类的run()方法的时候,先干点别的,再调用父类的run()方法。

这么一搞,是不是跟下面这种在IDE里手写一个Car的子类是类似的?

看看下面的手写版本的代码:

看看上面那个SafeCar作为Car的子类,是不是干了一样的事?

但是这个类需要你用双手提前写出来代码,而CGLIB Enhancer那种模式可以在系统运行期间动态的创建一个Car的子类出来,实现一样的效果

看到这里,各位应该理解这个动态创建类了!

限制Metaspace大小看看内存溢出效果

接着我们可以设置一下这个程序的JVM参数,限制他的Metaspace区域比较小一点,如下所示,我们把这个程序的JVM中的Metaspace区域设置为仅仅10m:

-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

接着我们可以在上述代码中做点手脚,大家看到上面的代码是有一个while循环的,所以他会不停的创建Car类的子类

我们在里面可以加入一个计数器,就是看看当前创建了多少个Car的子类了,如下所示:

在while循环外面加一个计数器,然后打印出当前创建了多少个类了。

接着大家用上述JVM参数来运行这个程序即可,可以看到如下所示的打印输出:

目前创建了263个Car类的子类了

Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
at net.sf.cglib.reflect.FastClass$Generator.create(FastClass.java:65)
at net.sf.cglib.proxy.MethodProxy.helper(MethodProxy.java:121)
at net.sf.cglib.proxy.MethodProxy.init(MethodProxy.java:75)
at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:226)
at com.limao.demo.jvm.Demo1$1.intercept(Demo1.java:22)
at com.limao.demo.jvm.Demo1$Car$$EnhancerByCGLIB$$7e5aa3a5_264.run(<generated>)
at com.limao.demo.jvm.Demo1.main(Demo1.java:30)
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
... 10 more

大家注意一下上述异常日志的两个地方,一个是在创建了263个类之后,10M的Metaspace区域就被耗尽了,接着就会看到异常中有如下的一个:

Caused by: java.lang.OutOfMemoryError: Metaspace。

这个OutOfMemoryError就是经典的内存溢出的问题,而且他明确告诉你,是Metaspace这块区域内存溢出了。

而且大家可以看到,一旦内存溢出,本来在运行的JVM进程直接会崩溃掉,你的程序会退出,这就是真实的内存溢出的日志。


小结

只要记得,合理分配Metaspace区域,同时避免无限制的动态生成类,一般这块区域其实都是比较安全的,不至于会触发内存溢出的。


相关文章
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
12天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
9天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
10 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
29天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10
|
28天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
51 2
|
1月前
|
存储 缓存 监控
聊聊JIT是如何影响JVM性能的!
聊聊JIT是如何影响JVM性能的!