1. @RestController
  2. public class MemoryTestController {
  3. @Autowired
  4. private PeopleSevice peopleSevice;
  5. /**
  6. * 案例2:模拟元空间OOM溢出
  7. */
  8. @RequestMapping("/metaSpaceOom")
  9. public void metaSpaceOom(){
  10. ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
  11. while (true){
  12. Enhancer enhancer = new Enhancer();
  13. enhancer.setSuperclass(People.class);
  14. // enhancer.setUseCache(false);
  15. enhancer.setUseCache(true);
  16. enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
  17. System.out.println("我是加强类,输出print之前的加强方法");
  18. return methodProxy.invokeSuper(o,objects);
  19. });
  20. People people = (People)enhancer.create();
  21. people.print();
  22. System.out.println(people.getClass());
  23. System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
  24. System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
  25. System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
  26. }
  27. }
  28. }

1-元空间存储数据类型

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

2-报错信息

java.lang.OutOfMemoryError: Metaspace

3-案例模拟

示例代码

发送请求

http://localhost:8080/metaSpaceOom

运行结果

我是加强类,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.PeopleEnhancerByCGLIB6ef22046_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)

4-JVM参数配置

-XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512K -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps
-Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log

5-原因及解决方案

JDK8后,元空间替换了永久代,元空间使用的是本地内存

原因:
1. 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
2. 应用长时间运行,没有重启
3. 元空间内存设置过小

解决方法:
因为该 OOM 原因比较简单,解决方法有如下几种:

1. 检查是否永久代空间或者元空间设置的过小
2. 检查代码中是否存在大量的反射操作
3. dump之后通过mat检查是否存在大量由于反射生成的代理类

6-分析及解决

6.1 查看监控

image.png
metatspace几乎已经被全部占用。

6.2 查看GC状态

image.png
可以看到,FullGC 非常频繁,而且我们的方法区,占用了59190KB/1024 = 57.8M空间,几乎把整个方法区空间占用,所以得出的结论是方法区空间设置过小,或者存在大量由于反射生成的代理类。

6.3 查看GC日志

  1. 2021-04-21T00:04:09.052+0800: 109.779: [GC (Metadata GC Threshold) [PSYoungGen: 174K->32K(19968K)] 14926K->14784K(60928K), 0.0013218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  2. 2021-04-21T00:04:09.054+0800: 109.780: [Full GC (Metadata GC Threshold) [PSYoungGen: 32K->0K(19968K)] [ParOldGen: 14752K->14752K(40960K)] 14784K->14752K(60928K), [Metaspace: 58691K->58691K(1103872K)], 0.0274454 secs] [Times: user=0.17 sys=0.00, real=0.03 secs]
  3. 2021-04-21T00:04:09.081+0800: 109.808: [GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] 14752K->14752K(60928K), 0.0009630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  4. 2021-04-21T00:04:09.082+0800: 109.809: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] [ParOldGen: 14752K->14752K(40960K)] 14752K->14752K(60928K), [Metaspace: 58691K->58691K(1103872K)], 0.0301540 secs] [Times: user=0.17 sys=0.00, real=0.03 secs]
  5. 2021-04-21T00:04:22.476+0800: 123.202: [GC (Metadata GC Threshold) [PSYoungGen: 3683K->384K(19968K)] 18435K->15144K(60928K), 0.0015294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  6. 2021-04-21T00:04:22.477+0800: 123.203: [Full GC (Metadata GC Threshold) [PSYoungGen: 384K->0K(19968K)] [ParOldGen: 14760K->14896K(40960K)] 15144K->14896K(60928K), [Metaspace: 58761K->58761K(1103872K)], 0.0299402 secs] [Times: user=0.16 sys=0.02, real=0.03 secs]
  7. 2021-04-21T00:04:22.508+0800: 123.233: [GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] 14896K->14896K(60928K), 0.0016583 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  8. 2021-04-21T00:04:22.509+0800: 123.235: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] [ParOldGen: 14896K->14751K(40960K)] 14896K->14751K(60928K), [Metaspace: 58761K->58692K(1103872K)], 0.0333369 secs] [Times: user=0.22 sys=0.02, real=0.03 secs]
  9. 2021-04-21T00:04:22.543+0800: 123.269: [GC (Metadata GC Threshold) [PSYoungGen: 229K->320K(19968K)] 14981K->15071K(60928K), 0.0014224 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  10. 2021-04-21T00:04:22.544+0800: 123.271: [Full GC (Metadata GC Threshold) [PSYoungGen: 320K->0K(19968K)] [ParOldGen: 14751K->14789K(40960K)] 15071K->14789K(60928K), [Metaspace: 58692K->58692K(1103872K)], 0.0498304 secs] [Times: user=0.42 sys=0.00, real=0.05 secs]
  11. 2021-04-21T00:04:22.594+0800: 123.321: [GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] 14789K->14789K(60928K), 0.0016910 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  12. 2021-04-21T00:04:22.596+0800: 123.322: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] [ParOldGen: 14789K->14773K(40960K)] 14789K->14773K(60928K), [Metaspace: 58692K->58692K(1103872K)], 0.0298989 secs] [Times: user=0.16 sys=0.02, real=0.03 secs]
  13. 2021-04-21T00:04:22.626+0800: 123.352: [GC (Metadata GC Threshold) [PSYoungGen: 0K->0K(19968K)] 14773K->14773K(60928K), 0.0013409 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  14. 2021-04-21T00:04:22.627+0800: 123.354: [Full GC (Metadata GC Threshold) [PSYoungGen: 0K->0K(19968K)] [ParOldGen: 14773K->14765K(40960K)] 14773K->14765K(60928K), [Metaspace: 58692K->58692K(1103872K)], 0.0298311 secs] [Times: user=0.17 sys=0.00, real=0.03 secs]
  15. 2021-04-21T00:04:22.657+0800: 123.384: [GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] 14765K->14765K(60928K), 0.0014417 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  16. 2021-04-21T00:04:22.659+0800: 123.385: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(19968K)] [ParOldGen: 14765K->14765K(40960K)] 14765K->14765K(60928K), [Metaspace: 58692K->58692K(1103872K)], 0.0253914 secs] [Times: user=0.30 sys=0.00, real=0.03 secs]
  17. 可以看到FullGC是由于方法区空间不足引起的,那么我们接下来分析到底是什么数据占用了大量的方法区。

6.4 分析dump文件

jvisualvm分析

导出dump文件,使用jvisualvm工具分析之:
image.png
image.png
对应的:
image.png

MAT分析

使用MAT分析:
image.png
image.png
image.png
通过线程分析如下图所示,可以定位到发生OOM的代码块
image.png
image.png

解决方案


那么我们可以想一下解决方案,每次是不是可以只加载一个代理类即可,因为我们的需求其实是没有必要如此加载的,当然如果业务上确实需要加载很多类的话,那么我们就要考虑增大方法区大小了,所以我们这里修改代码如下:
enhancer.setUseCache(true);
enhancer.setUseCache(false),选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。再看程序运行结果如下:
我是加强类哦,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.PeopleEnhancerByCGLIB6ef22046
totalClass:6901
activeClass:6901
我是加强类哦,输出print之前的加强方法
我是print本人
class com.atguiigu.jvmdemo.bean.PeopleEnhancerByCGLIB6ef22046
totalClass:6901
activeClass:6901+

可以看到,几乎不变了,方法区也没有溢出。到此,问题基本解决,再就是把while循环去掉。