У меня довольно большое приложение Java ee с огромным classpath, выполняющим большую обработку xml. В настоящее время я пытаюсь ускорить некоторые из моих функций и найти медленные пути кода с помощью выборочных профилировщиков.
Одна вещь, которую я заметил, заключается в том, что особенно части нашего кода, в которых мы имеем такие вызовы TransformerFactory.newInstance(...)
, крайне медленны. Я проследил это до FactoryFinder
метода, findServiceProvider
всегда создающего новый ServiceLoader
экземпляр. В ServiceLoader
Javadoc я нашел следующее примечание о кешировании:
Поставщики расположены и создаются лениво, то есть по требованию. Загрузчик службы поддерживает кэш поставщиков, которые были загружены до сих пор. Каждый вызов метода итератора возвращает итератор, который сначала возвращает все элементы кэша в порядке создания экземпляров, а затем лениво находит и создает экземпляры всех оставшихся поставщиков, добавляя каждого из них в кэш по очереди. Кэш может быть очищен с помощью метода перезагрузки.
Все идет нормально. Это часть FactoryFinder#findServiceProvider
метода OpenJDKs :
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) {
...
}
}
Каждый звонок на findServiceProvider
звонки ServiceLoader.load
. Это создает новый ServiceLoader каждый раз. Таким образом, кажется, что механизм кэширования ServiceLoaders вообще не используется. Каждый вызов сканирует путь к классу для запрашиваемого ServiceProvider.
Что я уже пробовал:
- Я знаю, что вы можете установить системное свойство, например,
javax.xml.transform.TransformerFactory
чтобы указать конкретную реализацию. Таким образом, FactoryFinder не использует процесс ServiceLoader и его супер быстрый. К сожалению, это свойство jvm wide и влияет на другие процессы java, работающие в моем jvm. Например, мое приложение поставляется с Saxon и должно использовать.com.saxonica.config.EnterpriseTransformerFactory
У меня есть другое приложение, которое не поставляется с Saxon. Как только я установлю системное свойство, мое другое приложение не сможет запуститься, потому чтоcom.saxonica.config.EnterpriseTransformerFactory
его путь к классу отсутствует. Так что это, кажется, не вариант для меня. - Я уже провел рефакторинг каждого места, где
TransformerFactory.newInstance
вызывается и кеширует TransformerFactory. Но в моих зависимостях есть разные места, где я не могу реорганизовать код.
Мои вопросы: почему FactoryFinder не использует ServiceLoader повторно? Есть ли способ ускорить весь этот процесс ServiceLoader, кроме использования системных свойств? Разве это не может быть изменено в JDK, чтобы FactoryFinder повторно использовал экземпляр ServiceLoader? Также это не относится только к одному FactoryFinder. Это поведение одинаково для всех классов FactoryFinder в javax.xml
пакете, на который я смотрел до сих пор.
Я использую OpenJDK 8/11. Мои приложения развернуты в экземпляре Tomcat 9.
Изменить: Предоставление более подробной информации
Вот стек вызовов для одного вызова XMLInputFactory.newInstance:
Где большинство ресурсов используется в ServiceLoaders$LazyIterator.hasNextService
. Этот метод вызывает getResources
ClassLoader для чтения META-INF/services/javax.xml.stream.XMLInputFactory
файла. Один только этот вызов занимает около 35 мс каждый раз.
Есть ли способ указать Tomcat лучше кэшировать эти файлы, чтобы они обслуживались быстрее?
источник
-D
флаг для вашегоTomcat
процесса? Например:-Djavax.xml.transform.TransformerFactory=<factory class>.
он не должен переопределять свойства для других приложений. Ваш пост хорошо описан и, возможно, вы уже пробовали, но я хотел бы подтвердить. См. Как установить системное свойство Javax.xml.transform.TransformerFactory , Как установить аргументы HeapMemory или JVM в TomcatОтветы:
35 мс звучит так, как будто есть время доступа к диску, и это указывает на проблему с кэшированием ОС.
Если на пути к классам есть какие-либо записи каталога / не-jar, это может замедлить работу. Также, если ресурс отсутствует в первом месте, которое проверено.
ClassLoader.getResource
может быть переопределено, если вы можете установить загрузчик класса контекста потока, либо через конфигурацию (я не трогал tomcat в течение многих лет), либо простоThread.setContextClassLoader
.источник
Я мог получить еще 30 минут для отладки и посмотреть, как Tomcat выполняет Resource Caching.
В частности
CachedResource.validateResources
(что можно найти на флеймографе выше) мне было интересно. Возвращается,true
еслиCachedResource
все еще действует:Похоже, у CachedResource действительно есть время для жизни (ttl). На самом деле в Tomcat есть способ настроить cacheTtl, но вы можете только увеличить это значение. Конфигурация кэширования ресурсов не очень гибкая, как кажется
Так что у моего Tomcat настроено значение по умолчанию 5000 мс. Это обмануло меня во время тестирования производительности, потому что между запросами у меня было чуть больше 5 секунд (просмотр графиков и прочего). Вот почему все мои запросы в основном выполнялись без кеша и
ZipFile.open
каждый раз вызывали такую нагрузку .Так как я не очень разбираюсь в настройке Tomcat, я еще не уверен, что является правильным решением здесь. Увеличение cacheTTL сохраняет кеши дольше, но не устраняет проблему в долгосрочной перспективе.
Резюме
Я думаю, что на самом деле здесь два преступника.
Классы FactoryFinder, не использующие ServiceLoader повторно. Возможно, есть веская причина, по которой они не используют их повторно, хотя я не могу придумать одну из них.
Tomcat высвобождает кеши через фиксированное время для ресурса веб-приложения (файлы в classpath - как
ServiceLoader
конфигурация)Добавьте к этому отсутствие определения системного свойства для класса ServiceLoader, и вы будете получать медленный вызов FactoryFinder каждую
cacheTtl
секунду.Пока я могу жить с увеличением cacheTtl в течение более длительного времени. Я также мог бы взглянуть на предложение Тома Хоутинса о переопределении,
Classloader.getResources
даже если я думаю, что это суровый способ избавиться от этого узкого места производительности. Возможно, стоит посмотреть на это.источник