JAR文件句柄:烦恼后清理!
在Ultra ESB中,我們使用特殊的熱交換類(lèi)加載器 ,該加載器使我們可以按需重新加載Java類(lèi)。 這使我們能夠從字面上熱交換我們的部署單元 -加載,卸載,使用更新的類(lèi)重新加載,以及正常地逐步退出-無(wú)需重啟JVM。
Windows:支持禁地
在Ultra ESB Legacy中 ,加載程序在Windows上可以正常運(yùn)行,但在較新的X版本上 ,似乎有些困難。 我們不支持將Windows作為目標(biāo)平臺(tái),因此并沒(méi)有太大的意義-直到最近,當(dāng)我們決定在Windows上支持非生產(chǎn)發(fā)行版時(shí)。 (我們的企業(yè)集成IDE UltraStudio在Windows上可以很好地運(yùn)行,因此Windows開(kāi)發(fā)人員都可以使用。)
TDD FTW
修復(fù)類(lèi)加載器很容易,所有測(cè)試都通過(guò)了; 但是我想通過(guò)一些額外的測(cè)試來(lái)支持我的修正,所以我寫(xiě)了一些新的測(cè)試。 其中大多數(shù)涉及在系統(tǒng)temp目錄下的子目錄中創(chuàng)建一個(gè)新的JAR文件,并使用熱交換類(lèi)加載器加載放置在JAR中的不同工件。 為了獲得更多有關(guān)最佳做法的榮譽(yù),我還確保添加一些清除邏輯,以通過(guò)FileUtils.deleteDirectory()刪除temp子目錄。
然后,事情發(fā)瘋了 。
拆解不再了。
在Linux和Windows上,所有測(cè)試都通過(guò)了; 但是最終的拆卸邏輯在Windows中失敗了,就在我刪除temp子目錄的那一刻。
在Windows上,我沒(méi)有l(wèi)sof的奢華; 幸運(yùn)的是, Sysinternals已經(jīng)有了我需要的東西: handle64 。
查找罪魁禍?zhǔn)追浅H菀?#xff1a;在刪除目錄樹(shù)之前,在tearDown()一個(gè)斷點(diǎn),然后運(yùn)行handle64 {my-jar-name}.jar 。
笨蛋
我的測(cè)試Java進(jìn)程持有測(cè)試JAR文件的句柄。
尋找泄漏
不行 我沒(méi)有
自然,我的第一個(gè)懷疑者是類(lèi)加載器本身。 我花了將近半小時(shí)反復(fù)遍歷類(lèi)加載器代碼庫(kù)。 沒(méi)運(yùn)氣。 一切似乎都堅(jiān)如磐石。
又名我的死神文件句柄
我最好的鏡頭是看是什么代碼打開(kāi)了JAR文件的處理程序。 因此,我為Java的FileInputStream和FilterInputStream編寫(xiě)了一個(gè)快速處理的補(bǔ)丁程序 ,該補(bǔ)丁程序?qū)⑥D(zhuǎn)儲(chǔ)獲取時(shí)間的堆棧跟蹤快照。 每當(dāng)線(xiàn)程保持流打開(kāi)時(shí)間過(guò)長(zhǎng)時(shí)。
此“泄漏轉(zhuǎn)儲(chǔ)程序”部分受我們的JDBC連接池的啟發(fā),該連接池檢測(cè)到未釋放的連接(受寬限期限制),然后轉(zhuǎn)儲(chǔ)借用它的線(xiàn)程的堆棧跟蹤-回到被借用的時(shí)間。 (榮譽(yù)給Sachini ,我以前的同事,實(shí)習(xí)生AdroitLogic 。)
泄漏,裸露!
果然,堆棧跟蹤揭示了罪魁禍?zhǔn)?#xff1a;
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 )知道了!
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 )但是,這并不能說(shuō)明全部情況。 如果URL.openStream()打開(kāi)JAR,為什么我們從try-with-resources塊返回時(shí)卻沒(méi)有關(guān)閉它?
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()); ... }瘋狂:
感謝Sun Microsystems使其成為OSS ,我可以瀏覽JDK源代碼,直到這個(gè)令人震驚的評(píng)論–一直到j(luò)ava.net.URLConnection :
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
來(lái)自sun.net.www.protocol.jar.JarURLConnection :
JarURLInputStream class extends FilterInputStream { JarURLInputStream(InputStream var2) { super (var2); } public void close() throws IOException { try { super .close(); } finally { if (!JarURLConnection. this .getUseCaches()) { JarURLConnection. this .jarFile.close(); } } } }如果( 因?yàn)?, 因?yàn)?) useCaches在默認(rèn)情況下為true ,那么我們感到非常驚訝!
讓Java緩存其JAR,但不要破壞我的測(cè)試!
JAR緩存可能會(huì)提高性能。 但這是否意味著我應(yīng)該在此之后停止清理-并且在每次測(cè)試后都留下雜散的文件?
(當(dāng)然,我可以說(shuō)file.deleteOnExit() ;但是由于我正在處理目錄層次結(jié)構(gòu),因此無(wú)法保證會(huì)按順序刪除內(nèi)容,并且會(huì)保留未刪除的目錄。)
因此,我想要一種清理JAR緩存的方法–或至少清除我的JAR條目; 完成之后,但在JVM關(guān)閉之前。
完全禁用JAR緩存-可能不是一個(gè)好主意!
URLConnection確實(shí)提供了一種避免緩存連接條目的選項(xiàng):
/** * 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禁用緩存,那將是完美的。 我們的類(lèi)加載器在打開(kāi)JAR時(shí)會(huì)立即緩存所有條目,因此不再需要再次打開(kāi)/讀取該文件。 但是,一旦打開(kāi)JAR,就無(wú)法禁用緩存。 因此,一旦我們的類(lèi)加載器打開(kāi)了JAR,就不會(huì)擺脫緩存的文件句柄-直到JVM本身關(guān)閉!
URLConnection還允許您默認(rèn)禁用所有后續(xù)連接的緩存:
/** * 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; }但是,如果您一次禁用它,則從那時(shí)起,整個(gè)JVM可能會(huì)受到影響-因?yàn)樗赡苓m用于所有基于URLConnection的實(shí)現(xiàn)。 正如我之前說(shuō)過(guò)的那樣,這可能會(huì)妨礙性能-更不用說(shuō)使測(cè)試脫離啟用緩存的實(shí)際行為了。
在兔子洞下(再次!):從
侵入性最小的選項(xiàng)是在我知道完成后從緩存中刪除我自己的JAR。
好消息是,緩存sun.net.www.protocol.jar.JarFileFactory已經(jīng)具有執(zhí)行該工作的close(JarFile)方法。
但是可悲的是,緩存類(lèi)是程序包專(zhuān)用的。 意味著無(wú)法在我的測(cè)試代碼中進(jìn)行操作。
反思救援!
多虧了反射,我所需要的只是一個(gè)小的“橋梁”,它將代表我訪(fǎng)問(wèn)和調(diào)用jarFactory.close(jarFile) :
JarBridge { 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(); } }在測(cè)試中,我只需要說(shuō):
JarBridge.closeJar(jarPath.toUri().toURL());就在刪除臨時(shí)目錄之前。
那么,要點(diǎn)是什么?
如果您不直接處理JAR文件,那么對(duì)您來(lái)說(shuō)無(wú)濟(jì)于事。 但是如果是這樣,您可能會(huì)遇到這種晦澀的“使用中的文件”錯(cuò)誤。 (這對(duì)于其他基于URLConnection的流同樣適用。)
如果您碰巧像我那樣(不幸),請(qǐng)回想一下,一個(gè)臭名昭著的博客寫(xiě)了一些駭人聽(tīng)聞的“ leak dumper”補(bǔ)丁JAR ,可以確切地告訴您JAR(或非JAR)泄漏的位置。
阿迪耶
翻譯自: https://www.javacodegeeks.com/2019/10/jar-file-handles-clean-up-after-your-mess.html
總結(jié)
以上是生活随笔為你收集整理的JAR文件句柄:烦恼后清理!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 中国最美的十大自然景观(全国旅游必去十大
- 下一篇: 苏州三星电子电脑有...(苏州三星电子电