Wag*_*ael 9 java serviceloader
我有一个相当大的Java ee应用程序,它具有执行大量xml处理的巨大类路径。目前,我正在尝试加快某些功能的速度,并通过采样分析器定位慢速代码路径。
我注意到的一件事是,特别是我们在其中调用类似代码的部分TransformerFactory.newInstance(...)非常慢。我跟踪下来到FactoryFinder方法findServiceProvider始终创建一个新的ServiceLoader实例。在ServiceLoader javadoc中,我发现了有关缓存的以下说明:
提供商的位置很懒,即按需实例化。服务加载器维护到目前为止已加载的提供者的缓存。每次迭代器方法的调用都会返回一个迭代器,该迭代器首先以实例化顺序生成高速缓存的所有元素,然后懒惰地定位和实例化任何剩余的提供程序,依次将每个提供程序添加到高速缓存中。可以通过reload方法清除缓存。
到目前为止,一切都很好。这是OpenJDKs FactoryFinder#findServiceProvider方法的一部分:
private static <T> T findServiceProvider(final Class<T> type)
throws TransformerFactoryConfigurationError
{
try {
return AccessController.doPrivileged(new PrivilegedAction<T>() {
public T run() {
final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
final Iterator<T> iterator = serviceLoader.iterator();
if (iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
}
});
} catch(ServiceConfigurationError e) {
...
}
}
Run Code Online (Sandbox Code Playgroud)
每一通findServiceProvider电话ServiceLoader.load。每次都会创建一个新的 ServiceLoader。这样,似乎根本没有使用ServiceLoaders缓存机制。每个调用都会扫描类路径以查找请求的ServiceProvider。
我已经尝试过的:
javax.xml.transform.TransformerFactory来指定特定的实现。这样,FactoryFinder不会使用ServiceLoader进程及其超快的速度。遗憾的是,这是jvm范围的属性,会影响在我的jvm中运行的其他java进程。例如,我的应用程序随Saxon一起提供并且应该使用,但com.saxonica.config.EnterpriseTransformerFactory我有另一个应用程序随Saxon一起提供。设置系统属性后,其他应用程序将无法启动,因为com.saxonica.config.EnterpriseTransformerFactory其类路径中没有。因此,这似乎不是我的选择。TransformerFactory.newInstance被称为a的地方,并缓存了TransformerFactory。但是在我的依赖项中有很多地方无法重构代码。我的问题是:为什么FactoryFinder不重用ServiceLoader?除了使用系统属性以外,是否有办法加快整个ServiceLoader进程?不能在JDK中对此进行更改,以便FactoryFinder重用ServiceLoader实例吗?此外,这并非特定于单个FactoryFinder。javax.xml到目前为止,我对软件包中所有FactoryFinder类的行为都相同。
我正在使用OpenJDK 8/11。我的应用程序部署在Tomcat 9实例中。
编辑:提供更多详细信息
这是单个XMLInputFactory.newInstance调用的调用堆栈:

使用最多资源的位置是ServiceLoaders$LazyIterator.hasNextService。此方法调用getResourcesClassLoader读取META-INF/services/javax.xml.stream.XMLInputFactory文件。每次通话大约需要35毫秒。
有没有一种方法可以指示Tomcat更好地缓存这些文件,以便更快地提供它们?
我可以再花 30 分钟来调试这个问题,并研究 Tomcat 如何进行资源缓存。
我特别CachedResource.validateResources感兴趣(可以在上面的火焰图中找到)。true如果CachedResource仍然有效则返回:
protected boolean validateResources(boolean useClassLoaderResources) {
long now = System.currentTimeMillis();
if (this.webResources == null) {
...
}
// TTL check here!!
if (now < this.nextCheck) {
return true;
} else if (this.root.isPackedWarFile()) {
this.nextCheck = this.ttl + now;
return true;
} else {
return false;
}
}
Run Code Online (Sandbox Code Playgroud)
看起来CachedResource实际上有一个生存时间 (ttl)。实际上Tomcat中有一种方法可以配置cacheTtl ,但你只能增加这个值。资源缓存配置看起来并不那么灵活。
所以我的Tomcat配置了默认值5000毫秒。这在进行性能测试时欺骗了我,因为我的请求之间有 5 秒多一点的时间(查看图表和其他内容)。这就是为什么我的所有请求基本上都在没有缓存的情况下运行,并且ZipFile.open每次都会触发如此繁重的操作。
因此,由于我对 Tomcat 配置并不是很有经验,所以我还不确定什么是正确的解决方案。增加cacheTTL可以使缓存更长,但从长远来看并不能解决问题。
概括
我认为这里实际上有两个罪魁祸首。
FactoryFinder 类不重用 ServiceLoader。他们不重复使用它们可能是有正当理由的——但我实在想不出一个原因。
Tomcat 在固定时间后驱逐 Web 应用程序资源的缓存(类路径中的文件 - 就像ServiceLoader配置)
再加上没有为 ServiceLoader 类定义系统属性,您每秒都会收到一次缓慢的 FactoryFinder 调用cacheTtl。
现在我可以忍受将cacheTtl 增加到更长的时间。我也可能会看看 Tom Hawtins 的重写建议,Classloader.getResources即使我认为这是摆脱性能瓶颈的一种严厉方式。不过,这可能值得一看。