Java11下如何卸载类加载器加载的sqljdbc_auth.dll?
这个问题在Java 11+里确实很棘手——因为从Java 9开始,模块化系统限制了对JDK内部API的访问,而且ClassLoader的内部实现也做了改动,导致Java 8里靠反射修改nativeLibraries的方案彻底失效了。咱们一步步拆解问题和解决方案:
首先,问题根源:每个自定义URLClassLoader都会独立加载sqljdbc_auth.dll,而Windows系统不允许同一个原生库被多个类加载器重复加载。Java 8里还能通过反射篡改ClassLoader的内部native库列表,但Java 11把这些内部细节都隐藏起来了。
最稳妥的解决方案:复用类加载器
既然重复加载的问题来自多个类加载器,那最简单的办法就是只创建一次自定义类加载器,所有数据库连接都复用它,这样native库只会被加载一次。修改你的代码如下:
// 静态变量保存JDBC专用类加载器,全局复用 private static ClassLoader jdbcClassLoader; private static final URL[] urls = // 这里是你的sql_jdbc.jar路径数组 private static void loadFile(){ ClassLoader currentCtxLoader = Thread.currentThread().getContextClassLoader(); // 第一次调用时初始化类加载器,之后直接复用 if (jdbcClassLoader == null) { jdbcClassLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader()); } Thread.currentThread().setContextClassLoader(jdbcClassLoader); try { Driver driver = (Driver) jdbcClassLoader.loadClass("com.microsoft.sqlserver.jdbc.SQLServerDriver").newInstance(); Connection connection = driver.connect( "jdbc:sqlserver://IP-addr:1433;DatabaseName=db_name;SelectMethod=cursor;integratedSecurity=true", props ); // 执行数据库操作... // 记得关闭连接和资源 connection.close(); } finally { // 恢复原来的上下文类加载器 Thread.currentThread().setContextClassLoader(currentCtxLoader); } // 不需要再卸载DLL了,因为类加载器会被复用 }
这个方案完全避开了类加载器卸载的问题,也是生产环境最推荐的做法。
备选方案:使用系统类加载器加载驱动
如果你的应用不需要类加载器隔离,可以把sql_jdbc.jar直接放到系统类路径里,然后用DriverManager直接获取连接——这样所有连接都会用系统类加载器,native库只会加载一次:
private static void loadFile(){ try (Connection connection = DriverManager.getConnection( "jdbc:sqlserver://IP-addr:1433;DatabaseName=db_name;SelectMethod=cursor;integratedSecurity=true", props )) { // 执行数据库操作... } catch (SQLException e) { e.printStackTrace(); } }
临时Hack方案:反射卸载Native库(不推荐生产环境)
如果因为业务限制必须使用多个类加载器,那可以通过反射访问JDK内部API来卸载native库,但这依赖JVM的内部实现,未来版本可能失效,而且需要额外的JVM启动参数。
修改你的unloadDllFile方法如下:
private synchronized static void unloadDllFile(String dllName, ClassLoader classLoader) throws Throwable { try { // 1. 获取ClassLoader内部的NativeLibraries实例 Field nativeLibrariesField = ClassLoader.class.getDeclaredField("nativeLibraries"); nativeLibrariesField.setAccessible(true); Object nativeLibraries = nativeLibrariesField.get(classLoader); // 2. 获取NativeLibraries中的libraries集合 Field librariesField = nativeLibraries.getClass().getDeclaredField("libraries"); librariesField.setAccessible(true); Set<?> libraries = (Set<?>) librariesField.get(nativeLibraries); // 3. 遍历找到目标DLL并卸载 Iterator<?> iterator = libraries.iterator(); while (iterator.hasNext()) { Object nativeLib = iterator.next(); Field pathField = nativeLib.getClass().getDeclaredField("path"); pathField.setAccessible(true); String libPath = (String) pathField.get(nativeLib); if (libPath.contains(dllName)) { // 调用NativeLibrary的unload方法 Method unloadMethod = nativeLib.getClass().getDeclaredMethod("unload"); unloadMethod.setAccessible(true); unloadMethod.invoke(nativeLib); iterator.remove(); // 触发GC回收类加载器 classLoader = null; System.gc(); System.runFinalization(); break; } } } catch (Exception e) { System.err.println("Failed to unload DLL: " + e.getMessage()); e.printStackTrace(); } }
注意:使用这个方法必须在JVM启动时添加参数:
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
这个参数允许你的代码访问JDK内部的jdk.internal.loader包,但这属于非法访问,会收到JVM警告,未来Java版本可能会彻底禁止这种操作。
内容的提问来源于stack exchange,提问作者user_3093890

