接上文:一波三折!记一次非堆内存泄漏(CXF+Jackson)的排查

问题依旧,找不到头绪

当我们自以为问题已经解决,补丁更新到客户生产环境后,意外发生了,系统依然故我,非堆内存仍然在平稳增加。

查看jvm内class的基本信息:

[root@iZ6h4s886i9imdZ ~]# jstat -class 1401519
Loaded      Bytes           Unloaded        Bytes       Time
634714      678273.2        20553           22886.8     825.14

class数量依然增长到 63万个了

由于客户生产环境不允许随意操作(无法调试),另外,也担心内存过大,dump抓取整个内存会导致系统宕机,暂时放弃了抓取内存分析的想法。

是classloader链的改法没有生效吗?究竟是哪些对象、或者class、或者classloader被外部持有了?
由于CXF2.7.3存在明显的classloader链,首先想到的是抓取一下所有classloader、以及它们之间父子关系,看是否形成了classloader链条。

尝试分析classloader关系

要抓取所有classloader对象的关系,这里要借用 JVM TI中的java.lang.instrument.Instrumentation对象,这需要借用java agent技术,
在运行期,将外部代码attach到正在运行的jvm上,根据Instrumentation上可以获取到的所有已加载class对象,收集classloader信息。
参考:分析jvm内存中classloader的关系
下面是一部分抓取内容:

  com.toone.v3.platform-34vbase-07aap-01service-orchestration [233]:
    COUNT: 95
  com.toone.v3.platform-34vbase-07aap-02service-orchestration-kafka [234]:
    COUNT: 1230
    java.net.URLClassLoader@1066f0c:
      COUNT: 236
      URLS:
      - file:/tmp/com.toone.itop.vbase.ws.impl.apiserver.cxfRewrite.VWSDynamicClientFactory@3d23dba2-1668562006596-classes/
    java.net.URLClassLoader@1092bb1c:
      COUNT: 236
      URLS:
      - file:/tmp/com.toone.itop.vbase.ws.impl.apiserver.cxfRewrite.VWSDynamicClientFactory@7a140ffb-1668554885599-classes/
    java.net.URLClassLoader@10ac4881:
      COUNT: 236
      URLS:
      - file:/tmp/com.toone.itop.vbase.ws.impl.apiserver.cxfRewrite.VWSDynamicClientFactory@75865fbe-1668561473152-classes/
    #更多 java.net.URLClassLoader 省略

从上面数据可以看出来,CXF生成的ClassLoader并没有形成parent依赖链,但是这些ClassLoader却无法卸载。
问题出在哪儿了呢?

柳暗花明

因为客户现场环境的问题,在模拟环境中始终无法重现。我们思路只好又回到了能否尽量模拟和客户环境一致的操作。
因为原模拟调用的webservice是另外开发的,返回值只是一个简单的String,所以考虑调用第三方的webservice来试验。
网上查找一下,找到这个天气预报的公开的webservice,来模拟调用。
结果一下子重现了class泄漏的场景。
接下来,抓取内存dump数据:jmap -dump:format=b,file=heapdump.phrof pid
使用jprofiler打开抓取的内存快照。
按如下操作:找到 堆遍历器 -> 当前对象集 -> 选择“检查” -> 选择“类&类加载器” -> 具有相同名称的类 -> 在新对象集中显示计算结果 -> 对话框中选择引用 -> 传入引用(Incoming),执行看一下效果:

在jprofiler中找到webservice生成的class (一般特征比较明显,如上面天气预报的className:cn.com.webxml.*)展开引用,我们会发现这些class被jackson所持有了。

再次检查代码

再次检查客户端代码,发现在使用CXF DynamicClientFactory动态调用webservice以后,将返回值使用 jackson转换成json对象使用了。至此,class泄漏的原因就清楚了。
我们猜想一下,也能够想到,jackson在处理对象转json时,需要解析class的属性,正常情况下,为避免反复解析同一个class,势必将解析结果缓存备用。在正常系统中,class数量一般是稳定的,不会出现大量一次性使用的class的情况,所以这个缓存策略是合理的。但是 CXF动态调用webservice + jackson结合在一起使用,就会出现class泄漏的情况。

解决办法

搞清楚了问题原因,解决方法就容易确定了,比如:

  • 不再使用jackson,改用自己重新实现一个反射tojson的方法并不复杂
  • 查看jackson文档,是否有清理缓存class的开关、或者方法
  • jackson由全局工具类,改为每次使用时创建、释放
  • 或者将webservice动态调用时产生的class/classloader缓存起来复用,一则解决class泄漏问题,二则由于减少了每次wsdl文件解析、java代码生成、java代码编译、class加载的过程,对webservice动态调用可能还有一些性能上的提升。当然,为了防止wsdl变动引起缓存变成脏数据的问题,可以通过对wsdl的MD5来判断缓存有效性。

由于不确定CXF的ClientImpl对象是否为重型对象,不确定此对象是否合适进行缓存使用、以及在并发场合下使用,
改造方案的主要缓存内容为ClassLoader;详细实现参考下一篇文章。


sswhsz
168 声望4 粉丝