起因

表面现象:客户生产环境,运行一段时间(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版本的用法        }    }}

再次实测成果如下:

问题至此,看上去仿佛完满解决了。但殊不知,前面还有一个微小的坑在等着咱们。

文章太长了,后续接着写。(给一点小提示,看文章题目)