起因
表面现象:客户生产环境,运行一段时间(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版本的用法
}
}
}
再次实测成果如下:
问题至此,看上去仿佛完满解决了。但殊不知,前面还有一个微小的坑在等着咱们。
文章太长了,后续接着写。(给一点小提示,看文章题目)
发表回复