В Ultra ESB мы используем специальный загрузчик классов с возможностью горячей замены, который позволяет нам перезагружать классы Java по требованию. Это позволяет нам буквально выполнять горячую замену наших модулей развертывания — загружать, выгружать, перезагружать с обновленными классами и постепенно отключать — без перезапуска JVM.
Окна: поддержка запретной земли
В Ultra ESB Legacy загрузчик отлично работал на Windows, но на более новой X-версии он, похоже, имел некоторые сбои. Мы не поддерживаем Windows как целевую платформу, поэтому это не имело большого значения — до недавнего времени, когда мы решили поддерживать непроизводственные дистрибутивы в Windows. (Наша IDE UltraStudio для корпоративной интеграции отлично работает в Windows, поэтому разработчики Windows, вы все охвачены.)
TDD FTW
Исправление загрузчика классов было легким, и все тесты проходили; но я хотел подкрепить свои исправления некоторыми дополнительными тестами, поэтому я написал несколько новых. Большинство из них включали создание нового файла JAR в подкаталоге системного временного каталога и использование загрузчика классов с возможностью горячей замены для загрузки различных артефактов, которые были помещены в JAR. Для дополнительной информации о лучших практиках я также добавил некоторую логику очистки для удаления временного подкаталога с помощью FileUtils.deleteDirectory()
.
А потом все сошло с ума .
И сноса больше не было.
Все тесты проходили как в Linux, так и в Windows; но окончательная логика разрыва не удалась в Windows, как раз в тот момент, когда я удаляю временный подкаталог.
Находясь на Windows, я не мог lsof
себе роскошь lsof
; к счастью, у Sysinternals уже было то, что мне было нужно: handle64
.
Найти виновника было довольно просто: нажмите tearDown()
останова в tearDown()
непосредственно перед вызовом удаления дерева каталогов и запустите handle64 {my-jar-name}.jar
.
Облом.
Мой тестовый Java-процесс содержал дескриптор тестового JAR-файла.
Охота на утечку
Нет, серьезно. Я не
Естественно, моим первым подозреваемым был сам загрузчик классов. Я потратил почти полчаса, снова и снова просматривая кодовую базу загрузчика классов. Не повезло. Все казалось твердым.
«Утечка»; ака мой мрачный жнец для файловых ручек
Лучше всего было посмотреть, какой фрагмент кода открыл обработчик для файла JAR. Поэтому я написал патч quick-n-dirty для Java FileInputStream
и FilterInputStream
который будет выводить снимки трассировки стека во время получения; всякий раз, когда поток удерживает поток открытым слишком долго.
Этот «дампер утечки» был частично вдохновлен нашим пулом соединений JDBC, который обнаруживает невыпущенные соединения (с льготным периодом), а затем выводит трассировку стека потока, который его заимствовал, обратно во время его заимствования. ( Слава Сачини , моему бывшему коллеге-практиканту в AdroitLogic .)
Утечка разоблачена!
Конечно же, трассировка стека выявила виновника:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
id: 174 created: 1570560438355 --filter-- java.io.FilterInputStream.<init>(FilterInputStream.java: 13 ) java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java: 81 ) java.util.zip.ZipFile$ZipFileInflaterInputStream.<init>(ZipFile.java: 408 ) java.util.zip.ZipFile.getInputStream(ZipFile.java: 389 ) java.util.jar.JarFile.getInputStream(JarFile.java: 447 ) sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java: 162 ) java.net.URL.openStream(URL.java: 1045 ) org.adroitlogic.x.base.util.HotSwapClassLoader.loadSwappableClass(HotSwapClassLoader.java: 175 ) org.adroitlogic.x.base.util.HotSwapClassLoader.loadClass(HotSwapClassLoader.java: 110 ) org.adroitlogic.x.base.util.HotSwapClassLoaderTest.testServiceLoader(HotSwapClassLoaderTest.java: 128 ) sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java: 62 ) sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java: 43 ) java.lang.reflect.Method.invoke(Method.java: 498 ) org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java: 86 ) org.testng.internal.Invoker.invokeMethod(Invoker.java: 643 ) org.testng.internal.Invoker.invokeTestMethod(Invoker.java: 820 ) org.testng.internal.Invoker.invokeTestMethods(Invoker.java: 1128 ) org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java: 129 ) org.testng.internal.TestMethodWorker.run(TestMethodWorker.java: 112 ) org.testng.TestRunner.privateRun(TestRunner.java: 782 ) org.testng.TestRunner.run(TestRunner.java: 632 ) org.testng.SuiteRunner.runTest(SuiteRunner.java: 366 ) org.testng.SuiteRunner.runSequentially(SuiteRunner.java: 361 ) org.testng.SuiteRunner.privateRun(SuiteRunner.java: 319 ) org.testng.SuiteRunner.run(SuiteRunner.java: 268 ) org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java: 52 ) org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java: 86 ) org.testng.TestNG.runSuitesSequentially(TestNG.java: 1244 ) org.testng.TestNG.runSuitesLocally(TestNG.java: 1169 ) org.testng.TestNG.run(TestNG.java: 1064 ) org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java: 72 ) org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java: 123 ) |
Попался!
1
2
3
4
5
|
java.io.FilterInputStream.<init>(FilterInputStream.java: 13 ) ... sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java: 162 ) java.net.URL.openStream(URL.java: 1045 ) org.adroitlogic.x.base.util.HotSwapClassLoader.loadSwappableClass(HotSwapClassLoader.java: 175 ) |
Но все же, это не рассказывало всю историю. Если URL.openStream()
открывает JAR, почему он не закрывается, когда мы возвращаемся из блока try-with-resources?
01
02
03
04
05
06
07
08
09
10
11
12
|
try (InputStream is = jarURI.toURL().openStream()) { byte [] bytes = IOUtils.toByteArray(is); Class<?> clazz = defineClass(className, bytes, 0 , bytes.length); ... logger.trace( 15 , "Loaded class {} as a swappable class" , className); return clazz; } catch (IOException e) { logger.warn( 16 , "Class {} located as a swappable class, but couldn't be loaded due to : {}, " + "trying to load the class as a usual class" , className, e.getMessage()); ... } |
В дикую природу: JarURLConnection
, URLConnection
и не только
Благодаря Sun Microsystems, которая сделала это OSS , я смог просмотреть исходный код JDK, вплоть до этого шокирующего комментария — вплоть до java.net.URLConnection
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
private static boolean defaultUseCaches = true ; /** * If <code>true</code>, the protocol is allowed to use caching * whenever it can. If <code>false</code>, the protocol must always * try to get a fresh copy of the object. * <p> * This field is set by the <code>setUseCaches</code> method. Its * value is returned by the <code>getUseCaches</code> method. * <p> * Its default value is the value given in the last invocation of the * <code>setDefaultUseCaches</code> method. * * @see java.net.URLConnection#setUseCaches(boolean) * @see java.net.URLConnection#getUseCaches() * @see java.net.URLConnection#setDefaultUseCaches(boolean) */ protected boolean useCaches = defaultUseCaches; |
Да, Java кэширует потоки JAR!
От sun.net.www.protocol.jar.JarURLConnection
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class JarURLInputStream extends FilterInputStream { JarURLInputStream(InputStream var2) { super (var2); } public void close() throws IOException { try { super .close(); } finally { if (!JarURLConnection. this .getUseCaches()) { JarURLConnection. this .jarFile.close(); } } } } |
Если (ну, потому что ) useCaches
по умолчанию имеет значение true
, нас useCaches
большой сюрприз!
Пусть Java кеширует свои JAR-файлы, но не нарушайте мой тест!
JAR-кэширование, вероятно, улучшит производительность; но значит ли это, что я должен прекратить очистку после — и оставлять после себя чужие файлы после каждого теста?
(Конечно, я мог бы сказать file.deleteOnExit()
; но, поскольку я имел дело с иерархией каталогов, не было никакой гарантии, что все будет удалено по порядку, а неуничтоженные каталоги останутся.)
Поэтому я хотел найти способ очистить кэш JAR — или хотя бы очистить только мою запись JAR; после того, как я закончу, но до того, как JVM закроется.
Отключение JAR-кэширования вообще — вероятно, не очень хорошая идея!
URLConnection
предлагает возможность избежать кэширования записей подключения:
01
02
03
04
05
06
07
08
09
10
|
/** * Sets the default value of the <code>useCaches</code> field to the * specified value. * * @param defaultusecaches the new value. * @see #getDefaultUseCaches() */ public void setDefaultUseCaches( boolean defaultusecaches) { defaultUseCaches = defaultusecaches; } |
Было бы идеально, если бы кэширование могло быть отключено для каждого файла / URL, как указано выше; наш загрузчик классов кэширует все записи, как только открывает JAR, поэтому ему больше не нужно открывать / читать этот файл снова. Однако, как только JAR открыт, на нем нельзя отключить кэширование; поэтому, как только наш загрузчик классов открыл JAR, избавиться от дескриптора кэшированного файла невозможно — пока сама JVM не закроется!
URLConnection
также позволяет отключить кэширование по умолчанию для всех последующих подключений:
01
02
03
04
05
06
07
08
09
10
|
/** * Sets the default value of the <code>useCaches</code> field to the * specified value. * * @param defaultusecaches the new value. * @see #getDefaultUseCaches() */ public void setDefaultUseCaches( boolean defaultusecaches) { defaultUseCaches = defaultusecaches; } |
Однако, если вы отключите его один раз, с этого момента может быть затронута вся JVM — поскольку это, вероятно, применимо ко всем реализациям на основе URLConnection
. Как я уже говорил, это может снизить производительность, не говоря уже об отклонении моего теста от реального поведения с поддержкой кэширования.
Вниз по кроличьей норе (снова!): JarFileFactory
вручную из JarFileFactory
Наименее инвазивный вариант — удалить мой JAR-файл из кэша, когда я знаю, что все готово.
И хорошие новости, кеш — sun.net.www.protocol.jar.JarFileFactory
— уже имеет метод close(JarFile)
который выполняет эту работу.
Но, к сожалению, класс кэша является закрытым для пакета; это означает, что нет способа манипулировать им из моего тестового кода.
Отражение на помощь!
Благодаря размышлениям все, что мне было нужно, — это один маленький «мост», который будет jarFactory.close(jarFile)
и вызывать jarFactory.close(jarFile)
от имени меня:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class JarBridge { static void closeJar(URL url) throws Exception { // JarFileFactory jarFactory = JarFileFactory.getInstance(); Class<?> jarFactoryClazz = Class.forName( "sun.net.www.protocol.jar.JarFileFactory" ); Method getInstance = jarFactoryClazz.getMethod( "getInstance" ); getInstance.setAccessible( true ); Object jarFactory = getInstance.invoke(jarFactoryClazz); // JarFile jarFile = jarFactory.get(url); Method get = jarFactoryClazz.getMethod( "get" , URL. class ); get.setAccessible( true ); Object jarFile = get.invoke(jarFactory, url); // jarFactory.close(jarFile); Method close = jarFactoryClazz.getMethod( "close" , JarFile. class ); close.setAccessible( true ); //noinspection JavaReflectionInvocation close.invoke(jarFactory, jarFile); // jarFile.close(); ((JarFile) jarFile).close(); } } |
И в моем тесте я просто должен сказать:
1
|
JarBridge.closeJar(jarPath.toUri().toURL()); |
Прямо перед удалением временного каталога.
Итак, что на вынос?
Ничего особенного для вас, если вы не имеете дело непосредственно с файлами JAR; но если это так, вы можете столкнуться с такими неясными ошибками «файл используется». (Это будет справедливо и для других потоков на основе URLConnection
.)
Если вам так (не) повезло, как и мне, просто вспомните, что какой-то небезызвестный блоггер написал какой-то хакерский патч JAR «утечки утечки», который бы точно показал вам, где находится ваша утечка JAR (или не JAR).
Прощайте!
Смотрите оригинальную статью здесь: JAR File Handles: Очистка после беспорядка! Мнения, высказанные участниками Java Code Geeks, являются их собственными. |