起因
表面现象:客户生产环境,运行一段时间(10~20天)后,无奈连贯kafka服务,这个景象重复呈现。
通过pinpoint监控查看故障前后的jvm状态,意外发现一个以前从未注意过的问题:那就是非堆内存满了。
从上图能够看进去,非堆内存满了之后,零碎进行了频繁的FullGC,然而内存并没有失去回收。
借助pinpoint,咱们往前回溯从上次jvm启动后,非堆内存的变动,发现:
- 9月2日,重启后, 非堆内存占用:300M
- 9月9日 1.4G
- 9月16日 2.1G
- 9月23日 3G
- 9月29日 4G
个别状况下,对于内存,咱们会比拟关注 堆内存,个别的内存透露,也都产生在堆内存中。非堆内存个别出问题的很少,所以咱们关注也比拟少。
初步狐疑
印象中,非堆内存(或者metaspace)存储的内容包含:class对象、字符串常量池、java栈内存、本地native库调配的内存、DirectByteBuffer调配的内存;
(下面形容可能不精确)
因为零碎中很少应用DirectByteBuffer,所以首先狐疑嵌入jvm过程的本地native库rocksdb,作为小文件读写缓存,rocksdb嫌疑最大。
于是咱们写了一个小程序对rocksdb的内存占用进行剖析,试验场景:10G数据写入rocksdb和随机读写,非堆内存始终稳固在 300M,并没有增长。于是排除了rocksdb的嫌疑。
发现问题
在排除rocksdb的嫌疑后,再回想到最后查看系统日志时的内存溢出提醒:CallWebService:java.lang.OutOfMemoryError: Compressed class space,网上搜寻一番,找到一篇知乎小短文:
JVM调优中,压缩类空间(Compressed Class space)如何了解
看到上面阐明:
一般来说,均匀一个 Klass 大小能够当成 1K 来算,默认的 1G 大小能够存储 100 万的 Klass。如果遇到了 `java.lang.OutOfMemoryError: Compressed class space`,就是类太多了,须要联合具体情况去抉择 JVM 调优还是 bug 排查。
因为业务零碎失常启动状况下 class 大概是 3万个,这个溢出阐明零碎的class 靠近 100万了?
零碎呈现问题时,因为各种谬误较多,所以竟然疏忽了这个重要的错误信息,这也算是走了一段弯路。
联合出错地位:CallWebService,因为零碎中应用了CXF来动静调用webservice,动静调用webservice的过程蕴含了:java代码生成、class编译和加载的过程,
难道是这个过程存在class透露吗?
立刻开始口头,找到零碎应用的CXF版本:2.7.3,编写一个小程序来验证:
public static void main(String[] args) throws Exception { String wsdlUrl = "http://10.1.28.143:8094/services/test?wsdl"; String method = "AAAA"; Object[] params = { "1" }; for (int i = 0; i < 1000; i++) { Object[] result = invokeWebService(wsdlUrl, method, params); System.out.println(Arrays.asList(result)); } new CountDownLatch(1).await();}public static Object[] invokeWebService(String wsdlUrl, String method, Object... params) throws Exception { Client client = null; try { DynamicClientFactory factory = DynamicClientFactory.newInstance(); client = factory.createClient(wsdlUrl); return client.invoke(method, params); } finally { if (client != null) { client.destroy(); } }}
应用jconsole察看调用过程,果然class数量在一直增长,FullGC也不能回收。
第一次解决
就CXF2.7.3动静调用webservice可能存在class透露问题,在网上检索一番,如同没看到有相干的话题。
去maven核心仓库search.maven.org
查找CXF的最新版本,如下:
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-bundle-compatible</artifactId> <version>3.5.3</version> <type>bundle</type></dependency>
换上新版本再次试验,后果调用失常了。区别是 3.5.3版本Client对象提供一个close办法(2.7.3没有close办法)
public static Object[] invokeWebService(String wsdlUrl, String method, Object... params) throws Exception { Client client = null; try { DynamicClientFactory factory = DynamicClientFactory.newInstance(); client = factory.createClient(wsdlUrl); return client.invoke(method, params); } finally { if (client != null) { // client.destroy(); 2.7.3版本应用destroy办法 client.close(); // 3.5.3版本应用close办法 } }}
查看 CXF3.5.3版本close办法的内容:
static class DynamicClientImpl extends ClientImpl implements AutoCloseable { final ClassLoader cl; final ClassLoader orig; DynamicClientImpl(Bus bus, Service svc, QName port, EndpointImplFactory endpointImplFactory, ClassLoader l) { super(bus, svc, port, endpointImplFactory); cl = l; orig = Thread.currentThread().getContextClassLoader(); //保留原始的classloader } @Override public void close() throws Exception { destroy(); if (Thread.currentThread().getContextClassLoader() == cl) { Thread.currentThread().setContextClassLoader(orig); //还原原始的classloader } }}
原来,在创立DynamicClientImpl实例时保留了以后的上下文classloader,同时在close()时,对上下文classloader进行了还原。
咱们再一步跟踪下org.apache.cxf.endpoint.dynamic.DynamicClientFactory.createClient()
的逻辑:
public Client createClient(String wsdlUrl, QName service, ClassLoader classLoader, QName port, List<String> bindingFiles) { //为了演示不便,上面只摘抄了要害代码 //0、实例化clientimpl对象 ClientImpl client = new ClientImpl(bus, svc, port, getEndpointImplFactory()); //1、依据wsdl生成java代码 JCodeModel codeModel = intermediateModel.generateCode(null, elForRun); File src = new File(tmpdir, stem + "-src"); Object writer = JAXBUtils.createFileCodeWriter(src); codeModel.build(writer); //2、编译java代码 File classes = new File(tmpdir, stem + "-classes"); setupClasspath(classPath, classLoader); List<File> srcFiles = FileUtils.getFilesRecurse(src, ".+\\.java$"); compileJavaSrc(classPath.toString(), srcFiles, classes.toString())); //3、创立classloader URL[] urls = new URL[] { classes.toURI().toURL() }; ClassLoader cl = ClassLoaderUtils.getURLClassLoader(urls, classLoader); //4、加载class JAXBContext context = JAXBContext.newInstance(packageList, cl, contextProperties); JAXBDataBinding databinding = new JAXBDataBinding(); databinding.setContext(context); svc.setDataBinding(databinding); //5、将新的classloader设置到以后线程上下文中,这一步的目标,是在后续应用invoke办法调用webservice时,能从以后线程上下文classloader中找到webservice动静生成的类 ClassLoaderUtils.setThreadContextClassloader(cl); //6、TypeClass初始化 (这一步含意还不分明) ServiceInfo svcfo = client.getEndpoint().getEndpointInfo().getService(); TypeClassInitializer visitor = new TypeClassInitializer(svcfo, intermediateModel, allowWrapperOps()); visitor.walk(); return client;}
至此,class透露的起因应该比较清楚了:
起因是 CXF2.7.3在client.destroy()后, 短少上下文ClassLoader的还原,导致以后的ClassLoader变成了一个链,每动静调用一次,ClassLoader链就变长一次,导致所有加载的class都无奈卸载。
因为降级CXF波及工作量较大,咱们只需在CXF2.7.3之上,在调用client前后加上一段小小的逻辑,来手工还原classloader就行了。革新如下:
public static Object[] invokeWebService(String wsdlUrl, String method, Object... params) throws Exception { Client client = null; ClassLoader orig = Thread.currentThread().getContextClassLoader(); try { DynamicClientFactory factory = DynamicClientFactory.newInstance(); client = factory.createClient(wsdlUrl); return client.invoke(method, params); } finally { if (orig != Thread.currentThread().getContextClassLoader()) { Thread.currentThread().setContextClassLoader(orig); //为2.7.3版本手工还原classloader } if (client != null) { client.destroy(); //2.7.3版本的用法 } }}
再次实测成果如下:
问题至此,看上去仿佛完满解决了。但殊不知,前面还有一个微小的坑在等着咱们。
文章太长了,后续接着写。(给一点小提示,看文章题目)