(1) 案例问题

  • 查看系统的 GC 日志发现有大量的 Full GC 记录,类似如下日志:
    • 【Full GC(Metadata GC Threshold)xxxxx, xxxxx】“,Metadata 元数据区 频繁的被塞满,导致频繁触发 Full GC,进而会带动 CMS 老年代回收,还会回收 Metadata 区域本身;
  • 通过 jstat 观察 Metadata 区域的内存呈现一个波动的状态,先会不断增加,把 Metadata 区占满,然后就会触发一次 Full GC,接着 Metadata 区域的内存又会变小;
  • 问题总结:
    • 系统运行时,不停的有新的类产生被加载到 Metadata 区域,把 Metadata 区占满,接着触发一次 Full GC 回收掉 Metadata 区域中的部分类;
    • 如此不断反复循环,频繁导致 Full GC 的发生;

image.png

(2) 优化思路

  • 看看到底是什么类不停的被加载到 Metadata 区?;

    • 添加两个参数,追踪类加载核类卸载的情况:“-XX:TraceClassLoading -XX:TraceClassUnloading”;
    • Tomcat 的 catalina.out 日志文件中,输出如下类的信息:

      1. Loaded sun.reflect.GeneratedSerializationConstructorAccessor from __JVM_Defined_Class
    • 结论:JVM 运行期间不停的加载了大量的莫名其妙的类 “GeneratedSerializationConstructorAccessor”,不停的将 Metadata 占满,导致频繁 Full GC;

  • 为什么会频繁的创建这个奇怪的类,放入 Metadata 呢?
    • google 一下这个奇怪类的资料,发现与 Java 反射有关:
      • 在执行 Method.invoke() 这种反射代码时,JVM 会有个底层优化机制:在使用反射调用一定次数之后,就会动态生成一些奇怪的类放入 Metadata 区,下次你再执行反射的时候,就是直接调用这些类的方法;
    • 结论
      • 如果你在代码中大量用了类似上面反射的东西,那么 JVM 就会动态的生成一些类放入 Metadata 区域里;
      • 上面频繁加载奇怪的类,是由于系统中在不停的执行反射代码;

image.png

  • JVM 创建的这些奇怪了有什么特别的地方?
    • JVM 创建的这些奇怪的类,是 Class 类型的对象;
    • Class 对象都是 SoftReference 被软引用的,正常情况下不会回收,但是如果内存紧张时就会回收这些对象;
    • 软引用对象在 GC 时是否回收的判定规则:
      • clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB
        • “clock - timestamp”代表了一个软引用对象他有多久没被访问过了;
        • “freespace”代表JVM中的空闲内存空间;
        • “SoftRefLRUPolicyMSPerMB”代表每一MB空闲内存空间可以允许 SoftReference 对象存活多久,默认1000ms;
      • 例如,JVM 里有3000MB的空闲空间,那么那些奇怪的被软引用的 Class 对象,可以存活 3000*1000=3000秒,就是50分钟左右;
      • 一般 GC 时,JVM 内部或多或少总有一些空间内存的,所以基本上如果不是快发生 OOM 内存溢出了,一般软引用也不会被回收;
      • 结论
        • JVM 会随着反射代码的执行,动态的创建一些奇怪的类,它们的 Class 对象都是软引用,正常情况下,不会被回收,但是也不会应该快速增长才对;
  • JVM 创建的奇怪的类为什么会不停的变多?
    • 因为有开发人员将 SoftRefLRUPolicyMSPerMB 这个 JVM 启动参数设置为了 0,以为这样一来,任何软引用对象就可以尽快释放掉,不用留存,尽量给内存释放空间出了;
    • 实际上会发生如下现象:
      • 这个参数设置为0之后,直接导致 clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB 这个公式的右半边是0,就导致所有的软引用对象,比如 JVM 生成的那些奇怪的 Class 对象,刚创建出来就可能被一次 Young GC 给带着立马回收掉一些。
      • 接着 JVM 在反射代码执行的过程中,就会继续创建这种奇怪的类,在 JVM 的机制之下,会导致这种奇怪类越来越多。
      • 最终就会导致 Metaspace 区域被放满了,一旦 Metaspace 区域被占满了,就会触发 Full GC,然后回收掉很多类,接着再次重复上述循环;

image.png

  • 解决办法
    • -XX:SoftRefLRUPolicyMSPerMB=0”这个参数设置大一些,可以设置 1000ms,2000ms 或者 5000ms,提高这个数值,就是让反射过程中 JVM 自动创建的软引用的一些类的 Class 对象不要被随便回收,基本上 Metadata 区域的内存占用就基本稳定,不会来回大幅度波动;