FactoryFinder性能/不良缓存

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。

我已经尝试过的:

  1. 我知道您可以设置系统属性javax.xml.transform.TransformerFactory来指定特定的实现。这样,FactoryFinder不会使用ServiceLoader进程及其超快的速度。遗憾的是,这是jvm范围的属性,会影响在我的jvm中运行的其他java进程。例如,我的应用程序随Saxon一起提供并且应该使用,但com.saxonica.config.EnterpriseTransformerFactory我有另一个应用程序随Saxon一起提供。设置系统属性后,其他应用程序将无法启动,因为com.saxonica.config.EnterpriseTransformerFactory其类路径中没有。因此,这似乎不是我的选择。
  2. 我已经重构了每个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更好地缓存这些文件,以便更快地提供它们?

Wag*_*ael 2

我可以再花 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可以使缓存更长,但从长远来看并不能解决问题。

概括

我认为这里实际上有两个罪魁祸首。

  1. FactoryFinder 类不重用 ServiceLoader。他们不重复使用它们可能是有正当理由的——但我实在想不出一个原因。

  2. Tomcat 在固定时间后驱逐 Web 应用程序资源的缓存(类路径中的文件 - 就像ServiceLoader配置)

再加上没有为 ServiceLoader 类定义系统属性,您每秒都会收到一次缓慢的 FactoryFinder 调用cacheTtl

现在我可以忍受将cacheTtl 增加到更长的时间。我也可能会看看 Tom Hawtins 的重写建议,Classloader.getResources即使我认为这是摆脱性能瓶颈的一种严厉方式。不过,这可能值得一看。