背景:
以往数据类项目,有这样的一个业务场景:异构数据源整库和各库表业务数据量定时统计,时效性每天统计,数据源涉及200+数据源,表数据量较大,客户要求每天凌晨精确查询数据量,不允许通过数据元数据模糊查询表数据量,项目上利用了多线程并发定时统计汇聚,因涉及到数据源多样性,采用动态加载不同版本的数据库驱动实现jdbc连接,导致的务java.lang.OutOfMemoryError: Compressed class space错误,服务不可用
排查思路:
1、了解jdk8 压缩空间是什么?
jdk8内存结构
jdk8之前有perm这一整块内存来存klass等信息(参数里必不可少地会配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小),jdk8后改成了metaspace元数据存储空间
为啥需要压缩空间?
OOPS:原始对象指针(ordinary object pointers)
64bit的JVM出现后,OOPS的尺寸也变成了64bit,比之前的大了一倍。这会引入性能损耗——占的内存double了,并且同尺寸的CPU Cache要少存一倍的OOPS
使用了这个压缩功能,每个对象中的 Klass* 字段就会被压缩成 32bit(不是所有的 oop 都会被压缩的),总所周知 Klass* 指向的 Klass 在永久代(Java7 及之前)。但是在 Java8 及之后,永久代没了,有了一个 Metaspace,于是之前压缩指针 Klass* 指向的这块 Klass 区域有了一个名字 —— Compressed Class Space。Compressed Class Space 是 Metaspace 的一部分,默认大小为 1G。所以其实 Compressed Class Space 这个名字取得很误导,压缩的并不是 Klass,而是 Klass*
metaspace元数据存储空间组成?
- Klass Metaspace
- NoKlass Metaspace
Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。这块内存是紧接着Heap的,和我们之前的perm一样,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。
NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。如果Klass Metaspace用完了,那就会OOM了,不过一般情况下不会,NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
2、jstat监控服务器内存以及gc情况
jstat-gc${pid} 10001000-gc:将显示与垃圾收集相关的统计信息${pid}:目标JVM进程ID1000:每10,00毫秒(即1秒)将打印一次统计信息。1000:将打印1000次迭代的统计信息S0C:第一个幸存区的大小S1C:第二个幸存区的大小S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小EC:伊甸园区的大小EU:伊甸园区的使用大小OC:老年代大小OU:老年代使用大小MC:方法区大小MU:方法区使用大小CCSC:压缩类空间大小CCSU:压缩类空间使用大小YGC:年轻代垃圾回收次数YGCT:年轻代垃圾回收消耗时间FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间
jstat命令样例:
在启动各个数据源物理表统计数据量的任务后,发现压缩空间巨增到700多M
程序代码并发流程如下:
排查出现主动加载类的代码段如下:
3、优化方案,全局缓存加载过的驱动