线上CXF版本: 3.1.12
使用场景: 定时任务,每次任务启动时调用jaxWsDynamicClientFactory.createClient(wsdlUrl)创建客户端client,每次任务结束前调用client.destroy()销毁客户端
问题: 任务运行一段时间后,metaspace溢出导致服务重启
分析dump文件: 发现程序创建了大量的URLClassLoader,链式引用,无法回收,最后导致MetaSpace溢出
CXF源码分析:
jaxWsDynamicClientFactory.createClient(wsdlUrl)最终会走如下方法
public Client createClient(String wsdlUrl, QName service, ClassLoader classLoader, QName port, List<String> bindingFiles) {
//传入的classLoader为null,因此获取当前线程上下文类加载器
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
LOG.log(Level.FINE, "Creating client from WSDL " + wsdlUrl);
WSDLServiceFactory sf = service == null ? new WSDLServiceFactory(this.bus, wsdlUrl) : new WSDLServiceFactory(this.bus, wsdlUrl, service);
sf.setAllowElementRefs(this.allowRefs);
Service svc = sf.create();
SchemaCollection schemas = ((ServiceInfo)svc.getServiceInfos().get(0)).getXmlSchemaCollection();
SchemaCompiler compiler = this.createSchemaCompiler();
DynamicClientFactory.InnerErrorListener listener = new DynamicClientFactory.InnerErrorListener(wsdlUrl);
Object elForRun = ReflectionInvokationHandler.createProxyWrapper(listener, JAXBUtils.getParamClass(compiler, "setErrorListener"));
compiler.setErrorListener(elForRun);
OASISCatalogManager catalog = (OASISCatalogManager)this.bus.getExtension(OASISCatalogManager.class);
this.hackInNewInternalizationLogic(compiler, catalog);
this.addSchemas(compiler.getOptions(), compiler, svc.getServiceInfos(), schemas);
this.addBindingFiles(bindingFiles, compiler);
S2JJAXBModel intermediateModel = compiler.bind();
listener.throwException();
JCodeModel codeModel = intermediateModel.generateCode((Object)null, elForRun);
StringBuilder sb = new StringBuilder();
boolean firstnt = false;
Iterator packages = codeModel.packages();
while(packages.hasNext()) {
JPackage jpackage = (JPackage)packages.next();
if (this.isValidPackage(jpackage)) {
if (firstnt) {
sb.append(':');
} else {
firstnt = true;
}
sb.append(jpackage.name());
}
}
JAXBUtils.logGeneratedClassNames(LOG, codeModel);
String packageList = sb.toString();
String stem = this.toString() + "-" + System.currentTimeMillis();
File src = new File(this.tmpdir, stem + "-src");
if (!src.mkdir()) {
throw new IllegalStateException("Unable to create working directory " + src.getPath());
} else {
try {
Object writer = JAXBUtils.createFileCodeWriter(src);
codeModel.build(writer);
} catch (Exception var34) {
throw new IllegalStateException("Unable to write generated Java files for schemas: " + var34.getMessage(), var34);
}
File classes = new File(this.tmpdir, stem + "-classes");
if (!classes.mkdir()) {
throw new IllegalStateException("Unable to create working directory " + classes.getPath());
} else {
StringBuilder classPath = new StringBuilder();
try {
setupClasspath(classPath, classLoader);
} catch (Exception var33) {
throw new RuntimeException(var33);
}
List<File> srcFiles = FileUtils.getFilesRecurse(src, ".+\\.java$");
if (srcFiles.size() > 0 && !this.compileJavaSrc(classPath.toString(), srcFiles, classes.toString())) {
LOG.log(Level.SEVERE, (new Message("COULD_NOT_COMPILE_SRC", LOG, new Object[]{wsdlUrl})).toString());
}
FileUtils.removeDir(src);
URL[] urls = null;
try {
urls = new URL[]{classes.toURI().toURL()};
} catch (MalformedURLException var32) {
throw new IllegalStateException("Internal error; a directory returns a malformed URL: " + var32.getMessage(), var32);
}
//该方法会new一个类加载器,并把传入的线程上下文类加载器作为父加载器,
ClassLoader cl = ClassLoaderUtils.getURLClassLoader(urls, classLoader);
Map<String, Object> contextProperties = this.jaxbContextProperties;
if (contextProperties == null) {
contextProperties = Collections.emptyMap();
}
JAXBContext context;
try {
if (StringUtils.isEmpty(packageList)) {
context = JAXBContext.newInstance(new Class[0], contextProperties);
} else {
context = JAXBContext.newInstance(packageList, cl, contextProperties);
}
} catch (JAXBException var31) {
throw new IllegalStateException("Unable to create JAXBContext for generated packages: " + var31.getMessage(), var31);
}
JAXBDataBinding databinding = new JAXBDataBinding();
databinding.setContext(context);
svc.setDataBinding(databinding);
ClientImpl client = new DynamicClientFactory.DynamicClientImpl(this.bus, svc, port, this.getEndpointImplFactory(), cl);
ServiceInfo svcfo = client.getEndpoint().getEndpointInfo().getService();
//把new的类加载器设置为当前线程上下文类加载器,因为定时任务线程一直存活,因此当前线程类加载器也一直存活,这也解释了为何dump文件中会出现类加载器链式引用
ClassLoaderUtils.setThreadContextClassloader(cl);
TypeClassInitializer visitor = new TypeClassInitializer(svcfo, intermediateModel, this.allowWrapperOps());
visitor.walk();
FileUtils.removeDir(classes);
return client;
}
}
}
ClassLoaderUtils.getURLClassLoader(urls, classLoader)方法如下
//该方法会new一个类加载器,并把传入的线程上下文类加载器作为父加载器,
public static ClassLoader getURLClassLoader(final URL[] urls, final ClassLoader parent) {
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
public ClassLoader run() {
//此处new了一个ClassLoader
return new URLClassLoader(urls, parent);
}
});
}
CXF bug描述: https://issues.apache.org/jira/browse/CXF-6852
虽然issue中说在3.1.11, 3.2.0版本已修复,但在3.1.12版本中,重复创建client时仍然会因为不断创建ClassLoader而导致MetaSpace溢出。
解决方案:
改为单例模式,不用每次定时任务启动都创建client