元空间GC

本文详细探讨了Java元空间(Metaspace)的构成、内存管理机制以及NashornScriptEngine在动态代码执行中的内存消耗问题,特别关注了类加载导致的内存溢出(OOM)和解决方法。
摘要由CSDN通过智能技术生成
  1. 基础知识
    1.1 Metaspace简介
    Metaspace元空间主要是存储类的元数据信息,我们的应用里加载的各种类描述信息,比如类名、属性、方法、访问限制等,按照一定的结构存储在Metaspace里。

1.1.1 Metaspace组成:
①Klass Metaspace

klass是class文件在jvm里的运行时数据结构, 而Klass Metaspace 就是用来存 klass的。

但xxx.class(反射)其实是存在heap中的, 是java.lang.Class 的一个对象实例。

这块内存紧挨着的Heap, 可以通过 -XX: CompressedClassSpaceSize参数来控制(默认1G)。

假如开启压缩指针就不会有这块内存, 这种情况下klass都会存在NoKlass Metaspace中

另外如果我们把-Xmx设置设置大于32G的话, 这块内存也会消失, 因为过大的内存会关闭压缩指针开关。

这块内存最多只会存在一块。

②NoKlass Metaspace

专门用来存klass相关的其他内存, 比如method, constantPool等。

这块内存是由多块内存组合起来的, 所以可以认为是不连续的内存块组成的。

这块内存是必须的, 虽然叫做NoKlass Metaspace, 但是也可以存klass的内容。

1.1.2 内存管理
①元空间的内存管理由元空间虚拟机来完成。

②类和其元数据的生命周期和其对应的类加载器是相同的。只要类加载器存活,其加载的类的元数据也是存活的,因而不被回收掉。

③元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些。

④每一个类加载器的存储区域都称作一个元空间,当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。

⑤在元空间虚拟机中存在一个全局的空闲组块列表。当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。

⑥当一个类加载器不再存活,那么其持有的组块将会被释放,并返回给全局组块列表。

⑦类加载器持有的组块又会被分成多个块,每一个块存储一个单元的元信息。组块中的块是线性分配(指针碰撞分配形式)。

⑧组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。

⑨在元空间的回收过程中没有重定位和压缩等操作。

1.2 OOM的场景
①由于反射类加载,动态代理生成的类加载等导致的,也就是说Metaspace的大小和加载类的数据有关系,加载的类越多metaspace占用的内存也就越大。

②大量的内存碎片。由于元空间回收过程没有重定位和压缩,所以会产生内存碎片。有很多内存碎片,会导致无法进行新的内存块分配。

③加载的类数量大于回收的数量。

1.3 class回收的条件
①类的所有实例已经回收

②类的Class实例没有引用

注意:不是一定会被立即回收

2 csc-pacific-flow-service js计算导致的metaspace oom
2.1 定位问题方法
2.1.1 修改JVM的参数
记录类加载及卸载信息

①使用-verbose:class

②-XX:+TraceClassLoading -XX:+TraceClassUnloading

2.1.2 使用arthas
线上使用有风险,谨慎操作(如内存占用,及有的命令会扫描大量的对象,都可能对线上带来风险)

arthas主要使用Java agent、asm等增强了技术,帮组用户调试、排查问题。

arthas文档如下,为定位问题,我们关注class/classloader这个模块

https://arthas.aliyun.com/doc/advanced-use.html

启动arthas后,attach到执行的进程。

①统计class的加载情况,隔一段时间执行一次

perfcounter | grep Classes

隔一段执行一次,可以发现有新的class对象生成,并加载的到内存中,

java.cls.loadedClasses、java.cls.unloadedClasses、sun.cls.initializedClasses、sun.cls.linkedClasses、sun.cls.verifiedClasses

②查找classloader

classloader -l 可以发现有大量的ScriptLoader,而且每个classloader只加载一个Class

③执行一次业务代码,统计具体classloader加载数量

classloader -l | grep jdk.nashorn.internal.runtime.ScriptLoader | wc -l

还可以导出对应的Class,进行反编译。

导出对应的class

dump -d savePath class_full_name

2.2 问题根源分析
2.2.1 搜索可能的Script执行代码
定位到业务调用代码为js的表达式计算。

2.2.2 源码分析执行过程
①从manager获取ScriptEngine

②由于nameAssociations是一个新对象的字段,所以会创建一个新的NashornScriptEngine

③创建一个新的nashornContext,这里需要注意,会创建了一个新的classCache

④执行Context下的Compile方法,首先从classCache获取对应的Class。但是获取为空,所以要动态编译一个class

⑤Context默认的环境变量_loader_per_compile为true,所以每次都会创建一个新的classLoader,来加载动态编译的Class

⑥加载并编译的表达式类对象

⑦在Compiler下的nextCompileUnitName,获取到的class名称就是 jdk/nashorn/internal/scripts/Script 1 1 1^eval_

⑧获取编译后的Class后,把Class加到缓存中

至此,整个执行过程分析结束。

2.3 问题根因
对我们的业务使用场景,ScriptEngineManager使用是当做局部变量使用,所以对于缓存的动态编译的class,在后续的调用中不会再次被使用,每次代码执行都会创建一个新的ScriptEngine,都会创建一个ClassLoader及动态编译、加载一个Class,所以会产生大量的Klass对象,回收导致大量元数据区的内存碎片,频繁触发full gc,最终导致metaspace oom。

2.4 解决方法
NashornScriptEngine官方文档

https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html#getParameter-java.lang.String-

从官方文档可知,NashornScriptEngine不是线程安全的,所以不能定义一个类级别的ScriptEngine变量,也就无法使用已经编译过的Class对象,所以这个工具类无法满足业务的需求。

结合没有合适的开源表达式计算工具,所以通过自定义表达式计算算法,解决这个问题。

  • 30
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值