线上频繁出现fullgc 的情况。
在JDK8里,Perm 区所有内容中
字符串常量移至堆内存
其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间
元空间特色
充分利用了Java语言规范:类及相关的元数据的生命周期与类加载器的一致
每个类加载器都有它的内存区域-元空间
只进行线性分配
不会单独回收某个类(除了重定义类 RedefineClasses 或类加载失败)
没有GC扫描或压缩
元空间里的对象不会被转移
如果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的时间长短。
Full GC时,指向元数据指针都不用再扫描,减少了Full GC的时间
很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了
元空间只有少量的指针指向Java堆 这包括:类的元数据中指向java.lang.Class实例的指针;数组类的元数据中,指向java.lang.Class集合的指针。
没有元数据压缩的开销
减少了GC Root的扫描(不再扫描虚拟机里面的已加载类的目录和其它的内部哈希表)
G1回收器中,并发标记阶段完成后就可以进行类的卸载
System.gc()方法的调用 在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
老年代(Tenured Gen)空间不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。
Metaspace区内存达到阈值 从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的。-XX:MetaspaceSize=64M,-XX:MetaspaceSize=256M。当metaspace空间不足会引发fullgc,之后jvm会根据实际对metaspace进行扩容。
分析内存
经过分析发现,metaspace空间持续加载和卸载类。
添加jvm参数:-XX:+TraceClassLoading -XX:+TraceClassUnloading
根据上图,显然可以得到结论,一直在加载的是 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和GsonJSON序列化的速度差不多,Jackson是最快的(用时Gson少大约600毫秒)。
JSON反序列化(JSON => Object) 测试样本数量为100000个,为了保证每个类库在测试中都能处理同一个样本,先把样本JSON对象保存在文件中。每个类库测试3次,每次循环测试10遍,去掉最快速度和最慢速度,对剩下的8遍求平均值作为最终的速,取3次测试中最好的平均速度作为最终的测试数据。
从测试数据可知,三个类库在反序列化上性能比较接近,Gson稍微差一些。
总结 把Java对象JSON序列化,Jackson速度最快,在测试中比Gson快接近50%,FastJSON和Gson速度接近。 把JSON反序列化成Java对象,FastJSON、Jackson速度接近,Gson速度稍慢,不过差距很小。