万隆的笔记 万隆的笔记
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
  • 大纲

  • 走近Java

  • 内存与垃圾回收

  • 字节码与类加载

  • 性能监控与调优

  • 监控与性能调优案例

    • JVM监控与性能调优案例概述
    • Tomcat堆溢出分析
    • 堆溢出
    • 方法区溢出
      • 报错信息
      • 案例模拟
      • 分析及解决
    • GC overhead limit exceeded
    • 线程溢出
    • 调整堆大小提高服务的吞吐量
    • JVM优化之JIT优化
    • 合理配置堆内存
    • CPU占用很高排查方案
    • G1并发执行的线程数对性能的影响
    • 调整垃圾回收器提高服务的吞吐量
    • 日均百万级订单交易系统如何设置JVM参数
    • 内存泄漏与内存溢出
  • Java虚拟机
  • 监控与性能调优案例
2022-03-18
目录

方法区溢出

# 方法区溢出

# 报错信息

java.lang.OutOfMemoryError: Metaspace 或 OutOfMemoryError:PermGen space

垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK 7及以前,方法区使用永久代实现。JDK 8及后,元空间替换了永久代,元空间使用的是本地内存。

# 案例模拟

同一个Spring Boot应用,主要如下,详见案例代码。请求 http://localhost:8080/metaSpaceOom

/**
 * 案例2:模拟元空间OOM溢出
 */
@RequestMapping("/metaSpaceOom")
public void metaSpaceOom(){
    ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
    while (true){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(People.class);
//            enhancer.setUseCache(false);
        enhancer.setUseCache(true);
        enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
            System.out.println("我是加强类,输出print之前的加强方法");
            return methodProxy.invokeSuper(o,objects);
        });
        People people = (People)enhancer.create();
        people.print();
        System.out.println(people.getClass());
        System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
        System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
        System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
    }
}

JVM参数配置:

-Xms80M
-Xmx80M
-XX:MetaspaceSize=80m
-XX:MaxMetaspaceSize=80m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof
-XX:SurvivorRatio=8
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-XX:+PrintGCDateStamps
-Xloggc:log/gc-oomMeta.log

PS:本人在测试的时候,短时间无法重现,因为发现-XX:MaxMetaspaceSize=80m并没有起作用,通过VisualVM发现元空间设置了1G的内存。建议使用main()方法去测试,这里我直接使用老师的dump文件分析。

-XX:MetaspaceSize、-XX:MaxMetaspaceSize

关于这几个参数,发现某些实际场景与文档并不一致,建议阅读一下JVM参数MetaspaceSize的误解 (opens new window)

JVM参数配置:

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

# 分析及解决

命令行查看查看GC状态:jstat GC 21052 1000 10

oom_case2_1

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

VisualVM分析dump文件:

oom_case2_2

MAT分析dump文件:首先我们先确定是哪里的代码发生了问题,首先可以通过线程来确定,因为在实际生产环境中,有时候是无法确定是哪块代码引起的OOM,那么我们就需要先定位问题线程,然后定位代码,如下图所示。

oom_case2_3

定位到代码以后,发现有使用到cglib动态代理,那么我们猜想一下问题是不是由于产生了很多代理类,接下来,我们可以通过包看一下我们的类加载情况

oom_case2_4

这里发现Method类的实例非常多,查看with outging references ,这里发现了很多的People类在调用相关的方法:

oom_case2_5

所以问题就是出现在加载了很多代理类。

解决方案:

那么我们可以想一下解决方案,每次是不是可以只加载一个代理类即可, 因为我们的需求其实是没有必要如此加载的,当然如果业务上确实需要加载很多类的话,那么我们就要考虑增大方法区大小了 ,所以我们这里修改代码如下:

enhancer.setUseCache(true); 

选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。 再看程序运行结果如下:

我是加强类哦,输出print之前的加强方法
我是print本人 
class com.atguiigu.jvmdemo.bean.People$$EnhancerByCGLIB$$6ef22046 
totalClass:6901 
activeClass:6901 
我是加强类哦,输出print之前的加强方法 
我是print本人 
class com.atguiigu.jvmdemo.bean.People$$EnhancerByCGLIB$$6ef22046 
totalClass:6901 
activeClass:6901+ 

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

常见原因与解决方法:

原因:

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

解决方法:

  1. 检查是否永久代空间或者元空间设置的过小
  2. 检查代码中是否存在大量的反射操作
  3. dump之后通过mat检查是否存在大量由于反射生成的代理类
#JVM监控与性能调优案例
上次更新: 5/28/2023, 10:57:53 PM
GC overhead limit exceeded

GC overhead limit exceeded→

最近更新
01
2025
01-15
02
Elasticsearch面试题
07-17
03
Elasticsearch进阶
07-16
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式