当前位置: 首页 > 后端技术 > Java

波折!记一次非堆内存泄漏(CXF+Jackson)排查

时间:2023-04-01 18:59:50 Java

造成表面现象:客户的生产环境,运行一段时间(10~20天)后,无法连接kafka服务,反复出现此现象.通过pinpointmonitoring查看故障前后的jvm状态,意外的发现了一个之前从来没有注意到的问题:那就是非堆内存满了。从上图可以看出,非堆内存被占满后,系统频繁的进行FullGC,但是内存并没有被回收。借助pinpoint,我们追溯自上次jvm启动以来非堆内存的变化,发现:9月2日,重启后,非堆内存使用量:300MSeptember91.4GSeptember162.1GSeptember233G9一般来说,对于内存,我们会比较关注堆内存,一般的内存泄漏也会发生在堆内存中。非堆内存一般很少有问题,所以我们关注的比较少。在最初的怀疑印象中,非堆内存(或元空间)存储的内容包括:类对象、字符串常量池、java栈内存、本地native库分配的内存、DirectByteBuffer分配的内存;(以上描述可能不准确)由于系统很少使用DirectByteBuffer,所以首先怀疑是jvm进程内嵌的本地原生库rocksdb,作为小文件读写缓存,rocksdb是最值得怀疑的。于是我们写了一个小程序来分析rocksdb的内存使用情况。测试场景:10G数据写入rocksdb,随机读写。非堆内存一直稳定在300M,没有增加。所以排除了rocksdb的嫌疑。找到问题后,排除rocksdb的嫌疑后,回忆起第一次查看系统日志时的内存溢出提示:CallWebService:java.中,如何理解压缩类空间(CompressedClassspace)见如下解释:一般来说,一个Klass的平均大小可以看做是1K,默认1G的大小可以存放100万个Klass。如果遇到`java.lang.OutOfMemoryError:Compressedclassspace`,说明class太多了,需要根据具体情况选择JVM调优或者bug排查。由于业务系统正常启动时大约有3万个类,这个溢出说明系统的类接近100万?当系统出现问题时,由于错误较多,忽略了这条重要的错误信息,也算少走弯路。结合报错位置:CallWebService,由于系统中使用了CXF来动态调用webservice,所以动态调用webservice的过程包括:java代码生成、类编译加载的过程,请问这个过程是否存在类泄漏?马上行动,发现系统使用的CXF版本:2.7.3,写个小程序验证一下:publicstaticvoidmain(String[]args)throwsException{StringwsdlUrl="http://10.1.28.143:8094/服务/测试?wsdl";字符串方法=“AAAA”;对象[]参数={“1”};for(inti=0;i<1000;i++){Object[]result=invokeWebService(wsdlUrl,method,params);System.out.println(Arrays.asList(结果));}newCountDownLatch(1).await();}publicstaticObject[]invokeWebService(StringwsdlUrl,Stringmethod,Object...params)throwsException{Clientclient=null;尝试{DynamicClientFactoryfactory=DynamicClientFactory.newInstance();客户端=factory.createClient(wsdlUrl);返回客户端调用(方法,参数);}finally{if(client!=null){client.destroy();}}}使用jconsole观察调用过程,发现class的数量在不断增加,FullGC无法回收。第一种解决方案是CXF2.7.3动态调用webservices可能存在classleakage问题。在网上搜索了一下,好像没有相关的话题。去maven中央仓库search.maven.org找到最新版本的CXF,如下:org.apache.cxfcxf-bundle-compatible3.5.3bundle用新版本再试,调用正常。不同的是3.5.3版本的Client对象提供了close方法(2.7.3没有close方法)publicstaticObject[]invokeWebService(StringwsdlUrl,Stringmethod,Object...params)throwsException{Client客户=空;尝试{DynamicClientFactoryfactory=DynamicClientFactory.newInstance();客户端=factory.createClient(wsdlUrl);返回客户端调用(方法,参数);}finally{if(client!=null){//client.destroy();版本2.7.3使用销毁方法client.close();//3.5.3版本使用close方法}}}查看CXF3.5.3版本close方法内容:staticclassDynamicClientImplextendsClientImplimplementsAutoCloseable{finalClassLoadercl;最终类加载器原件;DynamicClientImpl(Busbus,Servicesvc,QNameport,EndpointImplFactoryendpointImplFactory,ClassLoaderl){super(bus,svc,port,endpointImplFactory);cl=l;orig=Thread.currentThread().getContextClassLoader();//保存原图类加载器}@Overridepublicvoidclose()throwsException{destroy();如果(Thread.currentThread().getContextClassLoader()==cl){Thread.currentThread().setContextClassLoader(orig);//恢复原来的classloader}}}原来是创建DynamicClientImpl实例时保存了当前contextclassloader,close()时恢复了contextclassloader让我们进一步遵循org.apache.cxf.endpoint.dynamic.DynamicClientFactory.createClient()的逻辑:publicClientcreateClient(StringwsdlUrl,QNameservice,ClassLoaderclassLoader,QNameport,ListbindingFiles){//for演示为了方便,下面只摘录了关键代码//0。实例化clientimpl对象ClientImplclient=newClientImpl(bus,svc,port,getEndpointImplFactory());//1。根据wsdl生成java代码JCodeModelcodeModel=intermediateModel.generateCode(null,elForRun);文件src=新文件(tmpdir,stem+“-src”);对象编写器=JAXBUtils.createFileCodeWriter(src);codeModel.build(作者);//2。编译java代码Fileclasses=newFile(tmpdir,stem+"-classes");设置类路径(类路径,类加载器);列表<文件>srcFiles=FileUtils.getFilesRecurse(src,".+\\.java$");compileJavaSrc(classPath.toString(),srcFiles,classes.toString()));//3。创建类加载器URL[]urls=newURL[]{classes.toURI().toURL()};类加载器cl=ClassLoaderUtils.getURLClassLoader(urls,classLoader);//4。加载类JAXBContextcontext=JAXBContext.newInstance(packageList,cl,contextProperties);JAXBDataBinding数据绑定=newJAXBDataBinding();数据绑定.setContext(上下文);svc.setDataBinding(数据绑定);//5。将新的类加载器设置为当前线程上下文。这一步的目的是在使用invoke方法调用webservice时,从当前线程上下文类加载器中找到动态生成的类ClassLoaderUtils.setThreadContextClassloader(cl)。;//6。TypeClass初始化(这一步意思不清楚)ServiceInfosvcfo=client.getEndpoint().getEndpointInfo().getService();TypeClassInitializervisitor=newTypeClassInitializer(svcfo,intermediateModel,allowWrapperOps());visitor.walk();returnclient;}至此,classleak的原因应该很清楚了:原因是CXF2.7.3缺少client.destroy()后contextClassLoader的恢复,导致当前ClassLoader变成了链条。每动态调用一次,ClassLoader链就会变长,导致所有加载的类都被卸载。由于升级CXF涉及的工作量很大,我们只需要在CXF2.7.3之上调用客户端前后添加一小段逻辑,手动恢复类加载器即可。转换如下:publicstaticObject[]invokeWebService(StringwsdlUrl,Stringmethod,Object...params)throwsException{Clientclient=null;ClassLoaderorig=Thread.currentThread().getContextClassLoader();尝试{DynamicClientFactory工厂=DynamicClientFactory。新实例();客户端=factory.createClient(wsdlUrl);返回客户端调用(方法,参数);}finally{if(orig!=Thread.currentThread().getContextClassLoader()){Thread.currentThread().setContextClassLoader(orig);//手动恢复2.7.3版本的类加载器}if(client!=null){client.destroy();//Usageforversion2.7.3}}}实际测试结果如下:问题就出在这里,貌似完美解决了。但是殊不知,后面还有一个巨大的坑在等着我们。文章太长了,以后继续写。(给一点提示,看文章标题)