怎么清理句柄_JAR文件句柄:混乱后清理!
怎么清理句柄
在Ultra ESB中,我們使用特殊的熱交換類加載器 ,該加載器使我們可以按需重新加載Java類。 這使我們能夠從字面上熱交換我們的部署單元 -加載,卸載,使用更新的類重新加載,以及正常地逐步退出-無需重啟JVM。
Windows:支持禁地
在Ultra ESB Legacy中 ,加載程序在Windows上運行良好,但在較新的X版本上 ,似乎出現了一些問題。 我們不支持將Windows作為目標平臺,因此并沒有太大的意義-直到最近,當我們決定在Windows上支持非生產發行版時。 (我們的企業集成IDE UltraStudio在Windows上可以很好地運行,因此Windows開發人員都可以使用。)
TDD FTW
修復類加載器很容易,所有測試都通過了; 但是我想用一些額外的測試來支持我的修正,所以我寫了一些新的測試。 其中大多數涉及在系統temp目錄下的子目錄中創建一個新的JAR文件,并使用熱交換類加載器加載放置在JAR中的不同工件。 為了獲得更多有關最佳做法的功勞,我還確保添加一些清理邏輯,以通過FileUtils.deleteDirectory()刪除temp子目錄。
然后,事情變糟了 。
拆解不再了。
在Linux和Windows中,所有測試均通過了測試。 但是最終的拆卸邏輯在Windows中失敗了,就在我刪除temp子目錄的那一刻。
在Windows上,我沒有lsof的奢華; 幸運的是, Sysinternals已經有了我需要的東西: handle64 。
查找罪魁禍首非常容易:在刪除目錄樹之前,在tearDown()一個斷點,然后運行handle64 {my-jar-name}.jar 。
笨蛋
我的測試Java進程持有測試JAR文件的句柄。
尋找泄漏
不行 我沒有
自然,我的第一個懷疑者是類加載器本身。 我花了將近半小時反復遍歷類加載器代碼庫。 沒運氣。 一切似乎都堅如磐石。
又名我的死神文件句柄
我最好的鏡頭是看是什么代碼打開了JAR文件的處理程序。 因此,我為Java的FileInputStream和FilterInputStream編寫了一個快速處理的補丁程序 ,該補丁程序將轉儲獲取時間的堆棧跟蹤快照。 每當線程保持流打開時間過長時。
此“泄漏轉儲程序”部分受我們的JDBC連接池的啟發,該連接池檢測到未釋放的連接(受寬限期限制),然后轉儲借用它的線程的堆棧跟蹤-回到被借用的時間。 (榮譽給Sachini ,我以前的同事,實習生AdroitLogic 。)
泄漏,裸露!
果然,堆棧跟蹤揭示了罪魁禍首:
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 )但是,這并不能說明全部情況。 如果URL.openStream()打開JAR,為什么我們從try-with-resources塊返回時卻沒有關閉它?
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源代碼,直到這個令人震驚的評論–一直到java.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
來自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(); } } } }如果( 因為 , 因為 ) useCaches在默認情況下為true ,那么我們感到非常驚訝!
讓Java緩存其JAR,但不要破壞我的測試!
JAR緩存可能會提高性能。 但這是否意味著我應該在此之后停止清理-并且在每次測試后都留下雜散的文件?
(當然,我可以說file.deleteOnExit() ;但是由于我正在處理目錄層次結構,因此不能保證順序刪除所有內容,并且會保留未刪除的目錄。)
因此,我想要一種清理JAR緩存的方法–或至少清除我的JAR條目; 完成之后,但在JVM關閉之前。
完全禁用JAR緩存-可能不是一個好主意!
URLConnection確實提供了一種避免緩存連接條目的選項:
/** * 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還允許您默認禁用所有后續連接的緩存:
/** * 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的實現。 就像我之前說過的那樣,這可能會妨礙性能-更不用說使測試脫離啟用緩存的實際行為了。
在兔子洞下(再次!):從
侵入性最小的選項是在我知道完成后從緩存中刪除我自己的JAR。
好消息是,緩存sun.net.www.protocol.jar.JarFileFactory已經具有執行該工作的close(JarFile)方法。
但是可悲的是,緩存類是程序包專用的。 意味著無法在我的測試代碼中進行操作。
反思救援!
多虧了反射,我所需要的只是一個小的“橋梁”,它將代表我訪問和調用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(); } }在測試中,我只需要說:
JarBridge.closeJar(jarPath.toUri().toURL());就在刪除臨時目錄之前。
那么,要點是什么?
如果您不直接處理JAR文件,那么對您來說無濟于事。 但是如果是這樣,您可能會遇到這種晦澀的“使用中的文件”錯誤。 (這對于其他基于URLConnection的流同樣適用。)
如果您碰巧像我以前那樣(不幸),請回想一下,一個臭名昭著的博客寫了一些駭人的“ leak dumper”補丁JAR ,可以確切地告訴您JAR(或非JAR)泄漏的位置。
阿迪耶
翻譯自: https://www.javacodegeeks.com/2019/10/jar-file-handles-clean-up-after-your-mess.html
怎么清理句柄
總結
以上是生活随笔為你收集整理的怎么清理句柄_JAR文件句柄:混乱后清理!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓导航使用方法(安卓导航使用)
- 下一篇: linux系统调用命令(调用linux命