fastjson 导致fullgc频繁问题排查过程

背景

线上频繁出现fullgc 的情况。

前置知识点

fastjson 导致fullgc频繁问题排查过程_第1张图片

 

在JDK8里,Perm 区所有内容中

  • 字符串常量移至堆内存

  • 其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间

元空间特色

  • 充分利用了Java语言规范:类及相关的元数据的生命周期与类加载器的一致

  • 每个类加载器都有它的内存区域-元空间

  • 只进行线性分配

  • 不会单独回收某个类(除了重定义类 RedefineClasses 或类加载失败)

  • 没有GC扫描或压缩

  • 元空间里的对象不会被转移

  • 如果GC发现某个类加载器不再存活,会对整个元空间进行集体回收

在真正开始探索Full GC之前,我们需要先介绍几个概念

  • GC

    GC 全称为garbage collection,中文含义为垃圾回收,在jvm中的含义为回收无用内存空间

  • Young space

    中文名为年轻代或者新生代,为JVM 堆的一部分,由分代GC概念划分而来,保存生命周期较短的对象

  • Old space

    中文名为老年代或年老代,为JVM 堆的一部分,由分代GC概念划分而来,保存生命周期较长的对象

  • Minor GC

    minor gc指的是发生在年轻代或者说新生代(Young space)中的gc,也有人称其为young gc或者ygc,在下文中我们统一使用minor gc表示

  • Major GC (old gc)

    major gc指的是发生在老年代(Tenured space)中的gc,也有人称为old gc,o gc,cms gc等,在下文我们统一使用major gc表示

  • stop the world

    指的是用户线程在运行至安全点(safe point)或安全区域(safe region)之后,就自行挂起,进入暂停状态,对外的表现看起来就像是全世界都停止运转了一样,而不论何种gc算法,不论是minor gc还是major gc都会stop the world,区别只在于stop the world的时间长短。

GC

  • Full GC时,指向元数据指针都不用再扫描,减少了Full GC的时间

  • 很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了

  • 元空间只有少量的指针指向Java堆 这包括:类的元数据中指向java.lang.Class实例的指针;数组类的元数据中,指向java.lang.Class集合的指针。

  • 没有元数据压缩的开销

  • 减少了GC Root的扫描(不再扫描虚拟机里面的已加载类的目录和其它的内部哈希表)

  • G1回收器中,并发标记阶段完成后就可以进行类的卸载

常见引起fullgc的情况

  1. System.gc()方法的调用 在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

  2. 老年代(Tenured Gen)空间不足

    在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。

  3. Metaspace区内存达到阈值 从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的。-XX:MetaspaceSize=64M,-XX:MetaspaceSize=256M。当metaspace空间不足会引发fullgc,之后jvm会根据实际对metaspace进行扩容。

使用工具分析项目现状

分析内存

fastjson 导致fullgc频繁问题排查过程_第2张图片

 

排查出问题所在、

经过分析发现,metaspace空间持续加载和卸载类。

添加jvm参数:-XX:+TraceClassLoading -XX:+TraceClassUnloading

fastjson 导致fullgc频繁问题排查过程_第3张图片

 

fastjson源码解析

根据上图,显然可以得到结论,一直在加载的是 ASMSerializer_1_XXX 这个 class。

接下来就是查找 fastjson 在哪里一直重复加载了这个 class。

从 JSON.toJSONString 方法开始入手,进入到 com.alibaba.fastjson.JSON#toJSONString(java.lang.Object, com.alibaba.fastjson.serializer.SerializeConfig, com.alibaba.fastjson.serializer.SerializeFilter[], java.lang.String, int, com.alibaba.fastjson.serializer.SerializerFeature...) 方法中

再依次进入 getObjectWriter(clazz) -> config.getObjectWriter(clazz) -> put(clazz, createJavaBeanSerializer(clazz)) -> createJavaBeanSerializer(beanInfo) -> createASMSerializer(beanInfo) 方法中

在 createASMSerializer 中,有这样几行代码

......
// 拼接类名
String className = "ASMSerializer_" + seed.incrementAndGet() + "_" + clazz.getSimpleName();
String packageName = ASMSerializerFactory.class.getPackage().getName();
String classNameType = packageName.replace('.', '/') + "/" + className;
String classNameFull = packageName + "." + className;
ClassWriter cw = new ClassWriter();
// 然后这里就加载了 ASMSerializer_ 的类
cw.visit(V1_5 //
         , ACC_PUBLIC + ACC_SUPER //
         , classNameType //
         , JavaBeanSerializer //
         , new String[] { ObjectSerializer } //
);
......

所以,就是这里,每次调用到这里,就会 load ASMSerializer_1_T 到 metaspace 中。

而这部分代码在 ASMSerializerFactory 中。

回到用户代码,在 SerializeConfig config = new SerializeConfig() 这一行,进入 SerializeConfig 的无参构造,会调用它的有参构造。有参构造中有这么几行代码

if (asm) {
    asmFactory = new ASMSerializerFactory();
}

一切都很清晰了,由于一直创建 SerializeConfig,导致 ASMSerializerFactory 也会被重复创建,之后 ASMSerializerFactory 再调用 本类中的 createASMSerializer 方法的时候,就会导致重复加载 com.alibaba.fastjson.serializer.ASMSerializer_1_XXX

总结

  • fastjson使用注意:多线程下序列化及反序列化的问题

  • jvm参数调优:合理设置堆大小,matespace大小(类空间:非类空间=1:8,matespace = 类空间*2+非类空间)。

  • 序列化工具推荐

JSON序列化(Object => JSON) 测试样本数量为100000个,为了保证每个类库在测试中都能处理同一个样本,先把样本Java对象保存在文件中。每个类库测试3次,每次循环测试10遍,去掉最快速度和最慢速度,对剩下的8遍求平均值作为最终的速,取3次测试中最好的平均速度作为最终的测试数据。

fastjson 导致fullgc频繁问题排查过程_第4张图片

 

从测试数据可知,FastJSON和GsonJSON序列化的速度差不多,Jackson是最快的(用时Gson少大约600毫秒)。

JSON反序列化(JSON => Object) 测试样本数量为100000个,为了保证每个类库在测试中都能处理同一个样本,先把样本JSON对象保存在文件中。每个类库测试3次,每次循环测试10遍,去掉最快速度和最慢速度,对剩下的8遍求平均值作为最终的速,取3次测试中最好的平均速度作为最终的测试数据。

fastjson 导致fullgc频繁问题排查过程_第5张图片

 

从测试数据可知,三个类库在反序列化上性能比较接近,Gson稍微差一些。

总结 把Java对象JSON序列化,Jackson速度最快,在测试中比Gson快接近50%,FastJSON和Gson速度接近。 把JSON反序列化成Java对象,FastJSON、Jackson速度接近,Gson速度稍慢,不过差距很小。

你可能感兴趣的:(java,开发语言,后端)