咱们有一个Plugin的管理系统,能够实现Jar包的热装载,内部是基于一个Plugin管理类库PF4J,相似于OSGI,如今是GitHub上一个千星项目。 如下是该类库的官网介绍 > A plugin is a way for a third party to extend the functionality of an application. A plugin implements extension points declared by application or other plugins. Also a plugin can define extension points. With PF4J you can easily transform a monolithic java application in a modular application.java
大体意思就是,PF4J能够动态地加载Class文件。同时,它还能够实现动态地卸载Class文件。git
有个新需求,热更新Plugin的版本。也就是说,将已经被load进JVM的旧Plugin版本ubload掉,而后load新版本的Plugin。PF4J工做得很好。为了防止过时的Plugin太多,每次更新都会删除旧版本。然而,奇怪的事发生了: > - 调用File.delete()方法返回true,可是旧文件却还在 > - 手动去删除文件,报进程占用的错误 > - 当程序结束JVM退出以后,文件就跟着没了github
如下是简单的测试代码,目前基于PF4j版本3.0.1:windows
public static void main(String[] args) throws InterruptedException { // create the plugin manager PluginManager pluginManager = new DefaultPluginManager(); // start and load all plugins of application Path path = Paths.get("test.jar"); pluginManager.loadPlugin(path); pluginManager.startPlugins(); // do something with the plugin // stop and unload all plugins pluginManager.stopPlugins(); pluginManager.unloadPlugin("test-plugin-id"); try { // 这里并无报错 Files.delete(path); } catch (IOException e) { e.printStackTrace(); } // 文件一直存在,直到5s钟程序退出以后,文件自动被删除 Thread.sleep(5000); }
去google了一圈,没什么收获,反而在PF4J工程的Issues里面,有人报过相同的Bug,可是后面不了了之被Close了。api
看来只能本身解决了。 从上面的代码能够看出,PF4J的Plugin管理是经过PluginManager这个类来操做的。该类定义了一系列的操做:getPlugin(), loadPlugin(), stopPlugin(), unloadPlugin()...缓存
核心代码以下:oracle
private boolean unloadPlugin(String pluginId) { try { // 将Plugin置为Stop状态 PluginState pluginState = this.stopPlugin(pluginId, false); if (PluginState.STARTED == pluginState) { return false; } else { // 获得Plugin的包装类(代理类),能够认为这就是Plugin类 PluginWrapper pluginWrapper = this.getPlugin(pluginId); // 删除PluginManager中对该Plugin各类引用,方便GC this.plugins.remove(pluginId); this.getResolvedPlugins().remove(pluginWrapper); // 触发unload的事件 this.firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); // 热部署的一向做风,一个Jar一个ClassLoader:Map的Key是PluginId,Value是对应的ClassLoader // ClassLoader是自定义的,叫PluginClassLoader Map<string, classloader> pluginClassLoaders = this.getPluginClassLoaders(); if (pluginClassLoaders.containsKey(pluginId)) { // 将ClassLoader的引用也删除,方便GC ClassLoader classLoader = (ClassLoader)pluginClassLoaders.remove(pluginId); if (classLoader instanceof Closeable) { try { // 将ClassLoader给close掉,释放掉全部资源 ((Closeable)classLoader).close(); } catch (IOException var8) { throw new PluginRuntimeException(var8, "Cannot close classloader", new Object[0]); } } } return true; } } catch (IllegalArgumentException var9) { return false; } } public class PluginClassLoader extends URLClassLoader { }
代码逻辑比较简单,是标准的卸载Class的流程:将Plugin的引用置空,而后将对应的ClassLoader close掉以释放资源。这里特别要注意,这个ClassLoader是URLClassLoader的子类,而URLClassLoader实现了Closeable接口,能够释放资源,若有疑惑能够参考这篇文章。 类卸载部分,暂时没看出什么问题。app
加载Plugin的部分稍复杂,核心逻辑以下ide
protected PluginWrapper loadPluginFromPath(Path pluginPath) { // 获得PluginDescriptorFinder,用来查找PluginDescriptor // 有两种Finder,一种是经过Manifest来找,一种是经过properties文件来找 // 可想而知,这里会有IO读取操做 PluginDescriptorFinder pluginDescriptorFinder = getPluginDescriptorFinder(); // 经过PluginDescriptorFinder找到PluginDescriptor // PluginDescriptor记录了Plugin Id,Plugin name, PluginClass等等一系列信息 // 其实就是加载配置在Java Manifest中,或者plugin.properties文件中关于plugin的信息 PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginPath); pluginId = pluginDescriptor.getPluginId(); String pluginClassName = pluginDescriptor.getPluginClass(); // 加载Plugin ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor); // 建立Plugin的包装类(代理),这个包装类包含Plugin相关的全部信息 PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); // 设置Plugin的建立工厂,后续Plugin的实例是经过工厂模式建立的 pluginWrapper.setPluginFactory(getPluginFactory()); // 一些验证 ...... // 将已加载的Plugin作缓存 // 能够跟上述unloadPlugin的操做能够对应上 plugins.put(pluginId, pluginWrapper); getUnresolvedPlugins().add(pluginWrapper); getPluginClassLoaders().put(pluginId, pluginClassLoader); return pluginWrapper; }
有四个比较重要的类 > 1. PluginDescriptor:用来描述Plugin的类。一个PF4J的Plugin,必须在Jar的Manifest(pom的"manifestEntries"或者"MANIFEST.MF"文件)里标识Plugin的信息,如入口Class,PluginId,Plugin Version等等。 > 2. PluginDescriptorFinder:用来寻找PluginDescriptor的工具类,默认有两个实现:ManifestPluginDescriptorFinder和PropertiesPluginDescriptorFinder,顾名思义,对应两种Plugin信息的寻找方式。 > 3. PluginWrapper:Plugin的包装类,持有Plugin实例的引用,并提供了相对应信息(如PluginDescriptor,ClassLoader)的访问方法。 > 4. PluginClassLoader: 自定义类加载器,继承自URLClassLoader并重写了**loadClass()**方法,实现目标Plugin的加载。工具
回顾开头所说的问题,文件删不掉通常是别的进程占用致使的,文件流打开以后没有及时Close掉。可是咱们查了一遍上述过程当中出现的文件流操做都有Close。至此彷佛陷入了僵局。
换一个思路,既然文件删不掉,那就看看赖在JVM里面究竟是什么东西。 跑测试代码,而后经过命令jps查找Java进程id(这里是11210),而后用如下命令dump出JVM中alive的对象到一个文件tmp.bin: > jmap -dump:live,format=b,file=tmp.bin 11210
接着在内存分析工具MAT中打开dump文件,结果以下图:
发现有一个类com.sun.nio.zipfs.ZipFileSystem占了大半的比例(68.8%),该类被sun.nio.fs.WindowsFileSystemProvider持有着引用。根据这个线索,咱们去代码里面看哪里有调用FileSystem相关的api,果真,在PropertiesPluginDescriptorFinder中找到了幕后黑手(只保留核心代码):
/** * Find a plugin descriptor in a properties file (in plugin repository). */ public class PropertiesPluginDescriptorFinder implements PluginDescriptorFinder { // 调用此方法去寻找plugin.properties,并加载Plugin相关的信息 public PluginDescriptor find(Path pluginPath) { // 关注getPropertiesPath这个方法 Path propertiesPath = getPropertiesPath(pluginPath, propertiesFileName); // 读取properties文件内容 ...... return createPluginDescriptor(properties); } protected Properties readProperties(Path pluginPath) { Path propertiesPath; try { // 文件最终是经过工具类FileUtils去获得Path变量 propertiesPath = FileUtils.getPath(pluginPath, propertiesFileName); } catch (IOException e) { throw new PluginRuntimeException(e); } // 加载properties文件 ...... return properties; } } public class FileUtils { public static Path getPath(Path path, String first, String... more) throws IOException { URI uri = path.toUri(); // 其余变量的初始化,跳过 ...... // 经过FileSystem去加载Path,出现了元凶FileSystem!!! // 这里拿到FileSystem以后,没有关闭资源!!! // 隐藏得太深了 return getFileSystem(uri).getPath(first, more); } // 这个方法返回一个FileSystem实例,注意方法签名,是会有IO操做的 private static FileSystem getFileSystem(URI uri) throws IOException { try { return FileSystems.getFileSystem(uri); } catch (FileSystemNotFoundException e) { // 若是uri不存在,也返回一个跟此uri绑定的空的FileSystem return FileSystems.newFileSystem(uri, Collections.<string, string>emptyMap()); } } }
刨根问底,终于跟MAT的分析结果对应上了。原来PropertiesPluginDescriptorFinder去加载Plugin描述的时候是经过FileSystem去作的,可是加载好以后,没有调用FileSystem.close()方法释放资源。咱们工程里面使用的DefaultPluginManager默认包含两个DescriptorFinder:
protected PluginDescriptorFinder createPluginDescriptorFinder() { // DefaultPluginManager的PluginDescriptorFinder是一个List // 使用了组合模式,按添加的顺序依次加载PluginDescriptor return new CompoundPluginDescriptorFinder() // 添加PropertiesPluginDescriptorFinder到List中 .add(new PropertiesPluginDescriptorFinder()) // 添加ManifestPluginDescriptorFinder到List中 .add(new ManifestPluginDescriptorFinder()); }
最终咱们用到的实际上是ManifestPluginDescriptorFinder,可是代码里先会用PropertiesPluginDescriptorFinder加载一遍(不管加载是否成功持都会持了文件的引用),发现加载不到,而后再用ManifestPluginDescriptorFinder。因此也就解释了,当JVM退出以后,文件自动就删除了,由于资源被强制释放了。
本身写一个类继承PropertiesPluginDescriptorFinder,重写其中的readProperties()方法调用本身写的MyFileUtil.getPath()方法,当使用完FileSystem.getPath以后,把FileSystem close掉,核心代码以下:
public class FileUtils { public static Path getPath(Path path, String first, String... more) throws IOException { URI uri = path.toUri(); ...... // 使用完毕,调用FileSystem.close() try (FileSystem fs = getFileSystem(uri)) { return fs.getPath(first, more); } } private static FileSystem getFileSystem(URI uri) throws IOException { try { return FileSystems.getFileSystem(uri); } catch (FileSystemNotFoundException e) { return FileSystems.newFileSystem(uri, Collections.<string, string>emptyMap()); } } }
隐藏得如此深的一个bug...虽然这并非个大问题,但确实困扰了咱们一段时间,并且确实有同仁也碰到过相似的问题。给PF4J上发了PR解决这个顽疾,也算是对开源社区尽了一点绵薄之力,以防后续同窗再遇到相似状况。
文件没法删除,95%的状况都是由于资源未释放干净。 PF4J去加载Plugin的描述信息有两种方式,一种是根据配置文件plugin.progerties,一种是根据Manifest配置。默认的行为是先经过plugin.progerties加载,若是加载不到,再经过Manifest加载。 而经过plugin.progerties加载的方法,内部是经过nio的FileSystem实现的。而当经过FileSystem加载以后,直至Plugin unload以前,都没有去调用**FileSystem.close()**方法释放资源,致使文件没法删除的bug。
FileSystem的建立是经过FileSystemProvider来完成的,不通的系统下有不一样的实现。如Windows下的实现以下:
FileSystemProvider被建立以后会被缓存起来,做为工具类FIleSystems的一个static成员变量,因此FileSystemProvider是不会被GC的。每当FileSystemProvider建立一个FileSystem,它会把该FileSystem放到本身的一个Map里面作缓存,因此正常状况FileSystem也是不会被GC的,正和上面MAT的分析结果同样。而FileSystem的close()方法,其中一步就是释放引用,因此在close以后,类就能够被内存回收,资源得以释放,文件就能够被正常删除了
public class ZipFileSystem extends FileSystem { // FileSystem本身所对应的provider private final ZipFileSystemProvider provider; public void close() throws IOException { ...... // 从provider中,删除本身的引用 this.provider.removeFileSystem(this.zfpath, this); ...... } } public class ZipFileSystemProvider extends FileSystemProvider { // 此Map保存了全部被这个Provider建立出来的FileSystem private final Map<path, zipfilesystem> filesystems = new HashMap(); void removeFileSystem(Path zfpath, ZipFileSystem zfs) throws IOException { // 真正删除引用的地方 synchronized(this.filesystems) { zfpath = zfpath.toRealPath(); if (this.filesystems.get(zfpath) == zfs) { this.filesystems.remove(zfpath); } } } } ~~~</path,></string,></string,></string,>