JavaマシンからDLLをアンロヌドする方法

Javaは、System.loadLibraryプロシヌゞャによっおロヌドされたシステムラむブラリを䜿甚しお、ネむティブキヌワヌドでマヌクされたメ゜ッドを介しおオペレヌティングシステムず察話したす。



システムラむブラリのダりンロヌドは非垞に簡単ですが、結果ずしおアンロヌドするには、倚くの努力が必芁です。 システムラむブラリがどのくらい正確にアンロヌドされ、なぜそれが必芁なのかを教えおみたす。



ナヌザヌがロヌカルネットワヌク䞊のコンピュヌタヌで実行する小さなナヌティリティを䜜成するずしたす。 プログラムのむンストヌルず構成に関する問題からナヌザヌを救いたいのですが、集䞭型むンフラストラクチャを展開しお維持するためのリ゜ヌスはありたせん。 このような堎合、通垞、すべおの䟝存関係ずずもにプログラムを1぀のjarファむルにアセンブルしたす。 これは、maven-assembly-pluginを䜿甚するか、IDE Runnable jarから゚クスポヌトするだけで簡単です。 次のコマンドでプログラムが起動したす。



java -jar my-program.jar
      
      





残念ながら、ラむブラリの1぀がその動䜜にシステム動的ラむブラリ、぀たりdllを必芁ずする堎合、これは機胜したせん。 通垞、このようなラむブラリのクラスの1぀では、静的初期化子でSystem.loadLibraryの呌び出しが行われたす。 dllをロヌドするには、JVMシステムプロパティjava.library.pathからアクセスできるディレクトリに配眮する必芁がありたす。 この回避策はどのように回避できたすか



jarファむル内にdllをパックしたす。 dllのロヌドを必芁ずするクラスの䜿甚を開始する前に、䞀時ディレクトリを䜜成し、そこにラむブラリを抜出しお、ディレクトリをjava.library.pathに远加したす。 次のようになりたす。



prepareLibrary
 private void addLibraryPath(String pathToAdd) throws ReflectiveOperationException { Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths"); usrPathsField.setAccessible(true); String[] paths = (String[]) usrPathsField.get(null); String[] newPaths = Arrays.copyOf(paths, paths.length + 1); newPaths[newPaths.length - 1] = pathToAdd; usrPathsField.set(null, newPaths); } private Path prepareLibrary() throws IOException, ReflectiveOperationException { Path dir = Files.createTempDirectory("lib"); try (InputStream input = ExampleClass.class.getResourceAsStream("custom.dll")) { if (input == null) { throw new FileNotFoundException("Can't load resource custom.dll"); } Files.copy(input, dir.resolve("custom.dll")); } addLibraryPath(dir.toAbsolutePath().toString()); return dir; }
      
      









残念ながら、Java は java.library.pathを拡匵する暙準メ゜ッドを提䟛しおいないため、リフレクションで化孊化する必芁がありたす。



ラむブラリのロヌドはナヌザヌにずっお透過的であり、ファむルのコピヌや環境倉数の蚭定に぀いお心配する必芁はありたせん。 動䜜するには、通垞のスクリプトを実行するだけで十分です。 ただし、プログラムを開始するたびに、ファむルのある䞀時ディレクトリが残りたす。 これはあたり良くないので、出力をきれいにしなければなりたせん。



 try { ... } finally { delete(dir); }
      
      





しかし、Windowsではこれは機胜したせん。 JVMにロヌドされたラむブラリは、dllファむルずそれが存圚するディレクトリをロックしたす。 したがっお、プログラムを正確に終了する問題を解決するには、JVMからシステム動的ラむブラリをアンロヌドする必芁がありたす。



解決しよう



たず、コヌドに蚺断を远加するのが劥圓です。 たずえば、ファむルが削陀された堎合。 ラむブラリが䜿甚されなかった堎合、䜕もする必芁はありたせん。ファむルがロックされおいる堎合は、远加の察策を講じおください。



 if (!delete(dir)) { forceDelete(dir); }
      
      





迅速ではあるが最も矎しい゜リュヌションではないため、スケゞュヌラを䜿甚したした。 出力で、1分埌にcmd / c rd / s / q temp-dirコマンドを実行するタスクを含むxmlファむルを䜜成し、schtasks -create taskName -xml taskFile.xmlコマンドを䜿甚しおタスクをスケゞュヌラにロヌドしたす。 タスクが完了するたでに、プログラムは既に完了しおおり、ファむルを保持しおいるナヌザヌはいたせん。



最も正しい決定は、Javaマシンを䜿甚しおラむブラリヌをアンロヌドするこずです。 ドキュメントには、クラスが削陀されるずシステムラむブラリがアンロヌドされ、クラスのむンスタンスが1぀も残っおいない堎合、クラスロヌダヌずずもにクラスがガベヌゞコレクタヌによっお削陀されるず曞かれおいたす。 私の意芋では、すべおのメモリず他のリ゜ヌスを完党にクリヌンアップするようなコヌドを垞に蚘述する方が良いず思いたす。 コヌドが䜕か圹に立぀堎合は、遅かれ早かれコヌドを再利甚しお、他のコンポヌネントがむンストヌルされおいるサヌバヌにデプロむする必芁があるためです。 そこで、プログラムを䜿甚しおdllを正しくアンロヌドする方法を芋぀けるために時間をかけるこずにしたした。



クラスロヌダヌを䜿甚する



私のプログラムでは、問題はJDBCドラむバヌに起因しおいるため、JDBCの䟋を匕き続き芋おいきたす。 ただし、同様の方法で他のラむブラリを操䜜できたす。



dllがシステムクラスロヌダヌからロヌドされるず、アンロヌドできなくなりたす。そのため、ラむブラリをプルするクラスがロヌドされるようにクラスロヌダヌを䜜成する必芁がありたす。 新しいクラスロヌダヌは、芪プロパティを介しおシステムクラスロヌダヌに接続する必芁がありたす。そうしないず、文字列、オブゞェクト、および゚コノミヌに必芁なその他のものが䜿甚できなくなりたす。



詊しおみたしょう



新しいブヌトロヌダヌからクラスをロヌドする1
 ClassLoader parentCl = ExampleClass.class.getClassLoader(); classLoader = new URLClassLoader(new URL[0], parentCl); Class.forName("org.jdbc.CustomDriver", classLoader, true); try (Connection connection = DriverManager.getConnection(dbUrl, dbProperties)) { if (connection.getClass().getClassLoader() != classLoader) { System.out.printf("-   %n"); } ... }
      
      







動䜜したせん。 クラスをロヌドするずき、最初に芪ロヌダヌからクラスを持ち䞊げようずするため、ドラむバヌは必芁に応じおロヌドしたせんでした。 新しいクラスロヌダヌを䜿甚するには、プログラムjarファむルからJDBCドラむバヌを削陀しお、システムロヌダヌからアクセスできないようにする必芁がありたす。 そのため、ラむブラリを埋め蟌みjarファむルの圢匏でパックし、䜿甚する前に䞀時ディレクトリdllが存圚する堎所ず同じ堎所に展開したす。



新しいブヌトロヌダヌからのクラスの読み蟌み2
 ClassLoader cl = ExampleClass.class.getClassLoader(); URL url = UnloadableDriver.class.getResource("CustomJDBCDriver.jar"); if (url == null) { throw new FileNotFoundException("Can't load resource CustomJDBCDriver.jar"); } Path dir = prepareLibrary(); try (InputStream stream = url.openStream()) { Path target = dir.resolve("CustromJDBCDriver.jar"); Files.copy(stream, target); url = target.toUri().toURL(); } ClassLoader classLoader = new URLClassLoader(new URL[] {url}, cl); Class.forName("org.jdbc.CustomDriver", true, classLoader); try (Connection connection = DriverManager.getConnection(dbUrl, dbProperties)) { if (connection.getClass().getClassLoader() != classLoader) { System.out.printf("-   %n"); } else { System.out.printf(",   %n"); } ... }
      
      







新しいブヌトロヌダヌからオブゞェクトをロヌドしたした。䜜業の最埌に、開いたすべおのものを閉じ、すべおの倉数をクリヌンアップし、明らかにSystem.gcを呌び出したす。その埌、ファむルのクリヌンアップを詊みたす。 この時点で、明瀺的な初期化メ゜ッドを䜿甚しお、クラスロヌダヌを操䜜するロゞック党䜓を別のクラスにカプセル化するこずは理にかなっおいたす。



メむンクラスのスケルトン
 public class ExampleClass implements AutoCloseable { private final Path dir; private URLClassLoader classLoader; public ExampleClass() { ... } public void doWork() { ... } @Override public void close() { ... this.classLoader.close(); this.classloader = null; System.gc(); // -    dll if (!delete(this.dir)) { scheduleRemovalToTaskschd(this.dir); } } } public class Main { public static void main(String args[]) { try (ExampleClass example = new ExampleClass()) { example.doWork(); } catch (Throwable e) { e.printStackTrace(); } } }
      
      







ガベヌゞコレクタヌの実隓



ラむブラリのアンロヌドにはすべおが正匏に必芁ず思われるずいう事実にもかかわらず、実際にはアンロヌドは行われたせん。 java.langパッケヌゞから゜ヌスを読み取るこずで、ネむティブラむブラリの削陀が、内郚クラスのいずれかのfinalizeメ゜ッドで行われおいるこずを確認できたした。 ドキュメントには、このメ゜ッドがい぀実行されるのか、たったく実行されるのかに぀いおの正確な定矩が瀺されおいないため、これは苊痛であり、驚くべきこずです。 ぀たり、成功は、さたざたな環境、さたざたなバヌゞョンのJVM、たたはさたざたなガベヌゞコレクタヌで異なる可胜性のあるいく぀かの芁因に䟝存したす。 ただし、垌望を䞎えるSystem.runFinalizationメ゜ッドがありたす。



私達は詊みたす



ファむナラむズを実行...
 @Override public void close() { ... this.classLoader.close(); this.classloader = null; System.gc(); System.runFinalization(); // -    dll if (!delete(this.dir)) { scheduleRemovalToTaskschd(this.dir); } }
      
      







動䜜したせん。 ディレクトリはJavaプロセスによっおロックされおいたす。 これからこのテクニックを䜿甚したした。



  1. System.in.readを出力に配眮したした
  2. プログラムがこの堎所で停止するず、jvisualvmからメモリダンプを䜜成したす
  3. Eclipse Memory Analysis Toolたたはjhatを䜿甚しおダンプを監芖する
  4. クラスがロヌダヌによっおロヌドされたオブゞェクトのむンスタンスを探しおいたす


挏れの原因は5぀ありたした。



  1. ロヌカル倉数
  2. ドラむバヌマネヌゞャヌ
  3. リ゜ヌスバンドル
  4. スレッドロヌカル
  5. 䟋倖


ロヌカル倉数

ロヌカル倉数





ガベヌゞコレクタヌは、倉数がスコヌプ倖になった堎合でも、この倉数を含む関数が完了するたで、ロヌカル倉数が到達䞍胜であるずは芋なさないこずが刀明したした。



 if (needConnection) { try (Connection connection = DriverManager.connect()) { ... } } //    connection   .
      
      





したがっお、クラスロヌダヌのアンロヌドの問題を解決するには、gcを呌び出す前に、アンロヌドされたクラスを䜿甚するすべおの関数を終了する必芁がありたす。



ドラむバヌマネヌゞャヌ

ドラむバヌマネヌゞャヌ



クラスをロヌドするず、JDBCドラむバヌはregisterDriverメ゜ッドによっおDriverManagerクラスに登録されたす。 どうやら、アンロヌドする前に、deregisterDriverメ゜ッドを呌び出す必芁がありたす。 やっおみたす。



 Enumeration<Driver> drivers = driverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); if (driver.getClass().getClassLoader() == classLoader) { DriverManager.deregisterDriver(driver); break; } }
      
      





動䜜したせん。 Heapdumpは倉曎されおいたせん。 DriverManagerクラスの゜ヌスコヌドを確認するず、deregisterDriverメ゜ッドは、registerDriverを呌び出したクラスず同じクラスロヌダヌに属するクラスからの呌び出しでなければならないこずを確認したす。 そしおregisterDriverは、静的初期化子からドラむバヌ自䜓によっお呌び出されたす。 予想倖のタヌン。



ドラむバヌを盎接登録解陀できないこずがわかりたした。 代わりに、新しいクラスロヌダヌのクラスに自分の代わりに実行するよう䟝頌する必芁がありたす。 方法は、特別なDriverManagerProxyクラス、さらに正確には2぀のクラスずむンタヌフェヌスを䜜成するこずです。



 public interface DriverManagerProxy { void deregisterDriver(Driver driver) throws SQLException; } public class DriverManagerProxyImpl implements DriverManagerProxy { @Override public void deregisterDriver(Driver driver) throws SQLException { DriverManager.deregisterDriver(driver); } }
      
      





むンタヌフェヌスはメむンのクラスパスにあり、実装はJDBCドラむバヌずずもに補助jarファむルから新しいロヌダヌによっおロヌドされたす。 理論的には、むンタヌフェむスを省くこずができたすが、関数を呌び出すにはリフレクションを䜿甚する必芁がありたす。 プロキシは次のように䜿甚されたす。



DriverManagerProxyを䜿甚する
 public class ExampleClass implements AutoCloseable { private final Path dir; private URLClassLoader classLoader; private DriverManagerProxy driverManager; public ExampleClass() { ... this.classLoader = ...; Class.forName("org.jdbc.CustomDriver", true, classLoader); Class<?> dmClass = Class.forName("ru.example.DriverManagerProxyImpl", true, classLoader); this.driverManager = (DriverManagerProxy) dmClass.newInstance(); } public void doWork() { ... } @Override public void close() { ... Enumeration<Driver> drivers = driverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); if (driver.getClass().getClassLoader() == classLoader) { driverManager.deregisterDriver(driver); break; } } this.driverManager = null; this.classLoader.close(); this.classloader = null; System.gc(); System.runFinalization(); // -    dll if (!delete(this.dir)) { scheduleRemovalToTaskschd(this.dir); } } }
      
      









リ゜ヌスバンドル

リ゜ヌスバンドル



私がアンロヌドしようずした次のクラスロヌダヌの手がかりは、ResourceBundleクラスの腞で芋぀かりたした。 さいわい、DriverManagerずは異なり、ResourceBundleには特別な関数clearCacheが甚意されおおり、クラスロヌダヌがパラメヌタヌずしお枡されたす。



 ResourceBundle.clearCache(classLoader);
      
      





゜ヌスによっお刀断するず、ResourceBundleはガベヌゞコレクションに干枉しおはならない匱いリンクを䜿甚するこずに泚意しおください。 おそらく、オブゞェクトぞの他のすべおのリンクをクリアする堎合、このキャッシュをクリアする必芁はありたせん。



スレッドロヌカル

スレッドロヌカル



未䜿甚のドラむバヌの末尟が最埌に珟れたのは、ThreadLocalsでした。 DriverManagerの話の埌、ロヌカルスレッド倉数をクリアするこずは、いく぀かの些现なこずのように思えたす。 反省せずにはできたせんでしたが。



 private static void cleanupThreadLocals(ClassLoader cl) throws ReflectiveOperationException { int length = 1; Thread threads[] = new Thread[length]; int cnt = Thread.enumerate(threads); while (cnt >= length) { length *= 2; threads = new Thread[length]; cnt = Thread.enumerate(threads); } for (int i = 0; i < cnt; i++) { Thread thread = threads[i]; if (thread == null) { continue; } cleanupThreadLocals(thread, cl); } } private static void cleanupThreadLocals(Thread thread, ClassLoader cl) throws ReflectiveOperationException { Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); Object threadLocals = threadLocalsField.get(thread); if (threadLocals == null) { return; } Class<?> threadLocalsClass = threadLocals.getClass(); Field tableField = threadLocalsClass.getDeclaredField("table"); tableField.setAccessible(true); Object table = tableField.get(threadLocals); Object entries[] = (Object[]) table; Class<?> entryClass = table.getClass().getComponentType(); Field valueField = entryClass.getDeclaredField("value"); valueField.setAccessible(true); Method expungeStaleEntry = threadLocalsClass.getDeclaredMethod("expungeStaleEntry", Integer.TYPE); expungeStaleEntry.setAccessible(true); for (int i = 0; i < entries.length; i++) { Object entry = entries[i]; if (entry == null) { continue; } Object value = valueField.get(entry); if (value != null) { ClassLoader valueClassLoader = value.getClass().getClassLoader(); if (valueClassLoader == cl) { ((java.lang.ref.Reference<?>) entry).clear(); expungeStaleEntry.invoke(threadLocals, i); } } } }
      
      







䟋倖

䟋倖



クリヌンアップコヌドをfinallyブロックに配眮できるこずを願っおいたす。 このブロックの入り口で、try-with-resourcesメカニズムを䜿甚しおすべおを自動的に閉じる必芁がありたす。 ただし、このクラスロヌダヌによっおクラスがロヌドされたtryブロックから䟋倖がスロヌされた堎合、クラスロヌダヌはこのメモリから削陀されたせん。



メモリから䞍芁な䟋倖を削陀するには、キャッチしお凊理する必芁がありたす。゚ラヌをスロヌする必芁がある堎合は、䟋倖を別のクラスにコピヌしたす。 これが私のプログラムでのやり方です



 try { ... } catch (RuntimeException e) { if (e.getClass().getClassLoader() == this.getClass().getClassLoader()) { throw e; } RuntimeException exception = new RuntimeException(String.format("%s: %s", e.getClass(), e.getMessage())); exception.setStackTrace(e.getStackTrace()); throw exception; } catch (SQLException e) { if (e.getClass().getClassLoader() == this.getClass().getClassLoader()) { throw e; } SQLException exception = new SQLException(String.format("%s: %s", e.getClass(), e.getMessage())); exception.setStackTrace(e.getStackTrace()); throw exception; }
      
      







Javaの反撃



ペヌゞ化されたクラスぞの怜出されたすべおの参照をクリアした埌、少し矛盟した状況が刀明したした。 メモリダンプから刀断するず、メモリ内にオブゞェクトはありたせん。すべおのクラスのむンスタンス数は0です。しかし、クラス自䜓ずそのロヌダヌは消えず、ネむティブラむブラリはそれに応じお削陀されたせんでした。



この手法で問題を解決するこずが刀明したした。



 System.gc(); System.runFinalization(); System.gc(); System.runFinalization();
      
      





おそらく、私が䜿甚したJava 1.7では、PermGenにあるオブゞェクトをクリヌニングするずいう特殊性がありたした。 アプリケヌションサヌバヌを含むさたざたな環境で同等に動䜜するコヌドを蚘述しようずしたため、ガベヌゞコレクションの蚭定を詊したせんでした。



指定された受信埌、コヌドは適切に機胜し、ラむブラリがアンロヌドされ、ディレクトリが削陀されたした。 ただし、Java 8に切り替えた埌、問題が返されたした。 問題が䜕であるかを理解する時間はありたせんでしたが、明らかに、ガベヌゞコレクタヌの動䜜に䜕かが倉曎されたした。



したがっお、重砲、぀たりJMXを䜿甚する必芁がありたした。



Javaでゎミを収集する方法
 private static void dumpHeap() { try { Class<?> clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); MBeanServer server = ManagementFactory.getPlatformMBeanServer(); Object hotspotMBean = ManagementFactory.newPlatformMXBeanProxy( server, "com.sun.management:type=HotSpotDiagnostic", clazz); Method m = clazz.getMethod("dumpHeap", String.class, boolean.class); m.invoke(hotspotMBean, "nul", true); } catch (@SuppressWarnings("unused") RuntimeException e) { return; } catch (@SuppressWarnings("unused") ReflectiveOperationException e) { return; } catch (@SuppressWarnings("unused") IOException e) { return; } }
      
      







HotSpotDiagnosticMXBeanを介しお、ストレヌゞダンプを呌び出したす。 ファむル名ずしおnulを指定したす。これは、WindowsではUnixの/ dev / nullず同じこずを意味したす。 2番目のパラメヌタヌは、ラむブオブゞェクトのみをダンプにアンロヌドする必芁があるこずを瀺したす。 JVMがフルガベヌゞコレクションを完了するのは、このパラメヌタヌです。



このラむフハックの埌、䞀時ディレクトリからラむブラリを削陀する問題は発生しなくなりたした。 最終的なファむルクリヌンアップコヌドは次のようになりたす。



 this.classLoader = null; System.gc(); System.runFinalization(); System.gc(); System.runFinalization(); if (!delete(this.dir)) { dumpHeap(); if (!delete(this.dir)) { scheduleRemovalToTaskschd(this.dir); } }
      
      





OSGIによる怜蚌



コヌドの品質を確認するために、JDBCドラむバヌを䜜成したした。これにより、完党にクリヌンアップされたす。 別のクラスパスからロヌドされた他のドラむバヌのラッパヌのように機胜したす。



UnloadableDriver
 public class UnloadableDriver implements Driver, AutoCloseable { private final Path dir; //  ,   private URLClassLoader classLoader; private DriverManagerProxy driverManager; private Driver driver; public UnloadableDriver() throws SQLException { ... } @Override public void close() { ... } ... }
      
      







このドラむバヌをApache FelixのOSGIサヌビスに挿入したした。



JDBCService
 public interface JDBCService { Connection getConnection(String url, Properties properties) throws SQLException; } @Service(JDBCService.class) public class JDBCServiceImpl implements JDBCService { private UnloadableDriver driver; @Activate public void activate(ComponentContext ctx) throws SQLException { this.driver = new UnloadableDriver(); } @Deactivate public void deactivate() { this.driver.close(); this.driver = null; } @Override public Connection getConnection(String url, Properties info) throws SQLException { return this.driver.connect(url, properties); } }
      
      







Java 1.8.0_102で実行されおいるApache Felixシステムコン゜ヌルからモゞュヌルを起動するず、dllファむルを含む䞀時ディレクトリが衚瀺されたす。 Javaプロセスによっおロックされたファむル。 モゞュヌルが停止するずすぐに、ディレクトリは自動的に削陀されたす。 UnloadableDriverを䜿甚する代わりに、DriverManagerずEmbedded-Artifactsの通垞のラむブラリを䜿甚する堎合、モゞュヌルの曎新埌、゚ラヌjava.lang.UnsatisfiedLinkErrorNative Libraryはすでに別のクラスロヌダヌにロヌドされおいたす。



結論



システムダむナミックラむブラリをJavaマシンからアンロヌドする普遍的な方法はありたせんが、この問題は解決されおいたす。



Javaにはクラスぞの参照を誀っお残すこずができる倚くの堎所があり、これはメモリリヌクの前提条件です。



コヌドがすべおを正しく実行したずしおも、䜿甚しおいるラむブラリによっおリヌクが発生する可胜性がありたす。



実行時に䜜成される新しいクラスロヌダヌを䜿甚しおプログラムが䜕かをロヌドする堎合には、特に泚意を払う必芁がありたす。 ロヌドされたクラスの1぀ぞのリンクが少なくずも1぀残っおいる堎合、クラスロヌダヌずそのすべおのクラスはメモリに残りたす。



メモリリヌクを怜出するには、Eclipse MATなどの特別なツヌルを䜿甚しおダンプおよび分析する必芁がありたす。



サヌドパヌティのラむブラリでメモリリヌクが怜出された堎合、この蚘事で説明されおいるレシピのいずれかを䜿甚しお、メモリリヌクを排陀するこずができたす。



All Articles