Java: jar 被移除后,class 仍然被加载

Java: after jar is removed, class is still loaded

我试图重现一个错误,其中更新了一个 jar(通过 linux 框上的 rsync)然后抛出了 NoClassDefFoundError。更新后的 jar 没有变化,但我在想 class 加载时文件正在传输的事实...

我现在正在尝试重现该错误。

我的应用程序从只有一个 jar (/opt/test/myjar.jar)

的 class 路径开始

其他 jar 位于 myjar.jar (/opt/test/lib/mylib.jar).

相同路径的目录中

库已注册到 myjar.jar META-INF/MANIFEST.MF 中,此文本

Manifest-Version: 1.0
Built-By: FB
Class-Path: lib/mylib.jar

现在我编写了一些代码等待几秒钟,然后用 Class.forName("mylib.MyClass") 加载一些 class。

然后我会设置文件夹,启动java运行时,然后删除lib/mylib.jar文件,等待Class.forName失败。

而且代码 运行 没问题。我期待 NoClassDefFoundError。然后我重新运行代码,抛出 NoClassDefFoundError

然后我将mylib.jar重新添加到lib目录,重新运行,一切正常。

然后我用 -verbose:class 重新运行代码,删除了 lib/mylib.jar 然后这个日志出现了。

[Loaded mylib.MyClass from file:/opt/test/lib/mylib.jar`]

所以 class 加载发生在 jar 删除之后。我不明白为什么这项工作。 之前没有从 lib/mylib.jar 加载其他 classes。

Jdk 使用的是 OpenJDK Runtime Environment Corretto-8.302.08.1 (build 1.8.0_302-b08)

我不明白 JVM 如何从我刚删除的文件中加载 class。我认为 JVM 可能会在某处缓存这些文件(可能是因为它们已在 MANIFEST.MF 中注册)。

有人知道这种行为吗?

Ps。我测试了这个确切的过程,但使用的是真正的 jar 和 classes。如果没有人知道为什么,我可以构建一个测试项目。

您使用的系统没有强制文件锁定。例如,如果您在 Windows 下尝试了相同的操作,则无法覆盖或删除 .jar 文件。

class路径下的jar文件在JVM启动时打开,并在运行时间内保持打开状态。我们可以使用普通文件操作来演示行为:

Path p = Files.createTempFile(Paths.get(System.getProperty("user.home")),"test",".tmp");
try(FileChannel ch = FileChannel.open(p,
                                 StandardOpenOption.READ, StandardOpenOption.WRITE)) {
  System.out.println("opened " + p);
  int rc = new ProcessBuilder("rm", "-v", p.toString()).inheritIO().start().waitFor();
  System.out.println("rm ran with rc " + rc);
  int w = ch.write(StandardCharsets.US_ASCII.encode("test data"));
  System.out.println("wrote " + w + " bytes into " + p);
  ch.position(0);
  ByteBuffer bb = ByteBuffer.allocate(w);
  do ch.read(bb); while(bb.hasRemaining());
  bb.flip();
  System.out.println("read " + bb.remaining() + " bytes, "
                   + StandardCharsets.US_ASCII.decode(bb));
}
System.out.println("closed, reopening");
try(FileChannel ch = FileChannel.open(p,
                                 StandardOpenOption.READ, StandardOpenOption.WRITE)) {
  System.out.println("opened " + p);
}
catch(IOException ex) {
  System.out.println("Reopening " + p + ": " + ex);
}

打印类似

的东西
opened /home/tux/test722563514590118445.tmp
removed '/home/tux/test722563514590118445.tmp'
rm ran with rc 0
wrote 9 bytes into /home/tux/test722563514590118445.tmp
read 9 bytes, test data
closed, reopening
Reopening /home/tux/test722563514590118445.tmp: java.nio.file.NoSuchFileException: /home/tux/test722563514590118445.tmp

证明删除后,我们仍然可以从已打开的文件中写入和读取数据,因为只有条目已从目录中删除。 JVM 现在正在对一个没有名称的文件进行操作。但是一旦这个文件句柄被关闭,试图再次打开它就会失败,因为现在它真的消失了。


但是,覆盖文件是另一回事。打开现有文件时,我们访问同一个文件并进行更改。

所以

Path p = Files.createTempFile(Paths.get(System.getProperty("user.home")),"test",".tmp");
try(FileChannel ch = FileChannel.open(p,
                                 StandardOpenOption.READ, StandardOpenOption.WRITE)) {
  System.out.println("opened " + p);
  int w = ch.write(StandardCharsets.US_ASCII.encode("test data"));
  System.out.println("wrote " + w + " bytes into " + p);
  int rc = new ProcessBuilder("cp", "/proc/self/cmdline", p.toString())
      .inheritIO().start().waitFor();
  System.out.println("cp ran with rc " + rc);
  ch.position(0);
  ByteBuffer bb = ByteBuffer.allocate(w);
  do ch.read(bb); while(bb.hasRemaining());
  bb.flip();
  System.out.println("read " + bb.remaining() + " bytes, "
                   + StandardCharsets.US_ASCII.decode(bb));
}

产生类似

的东西
opened /home/tux/test7100435925076742504.tmp
wrote 9 bytes into /home/tux/test7100435925076742504.tmp
cp ran with rc 0
read 9 bytes, cp/proc/

显示对已打开文件的 read 操作导致了 cp 写入的内容,当然部分原因是缓冲区的大小已预先调整为 Java 应用程序写入的内容.这演示了当一些数据已经被读取并且应用程序试图根据它从旧版本中知道的内容来解释新数据时,覆盖打开的文件会如何造成严重破坏。


这导致了更新 jar 文件而不会使已经 运行ning 的 JVM 崩溃的解决方案。首先删除旧的 jar 文件,这让 JVM 运行 在将新版本复制到同一位置之前使用已经打开的、现在私有的旧文件。从系统的角度来看,你有两个不同的文件。当 JVM 终止时,旧的将不复存在。替换后启动的 JVM 将使用新版本。