Java:删除JAR后,类仍被加载

2022-07-15 00:00:00 java classloader jar

我试图重现一个错误,其中JAR被更新(通过Linux机器上的rsync),然后抛出NoClassDefFoundError。更新的JAR没有变化,但我在考虑这样一个事实,即文件在类加载时正在传输...

我现在正在尝试复制该错误。

我的应用程序启动时只有一个JAR的类路径(/opt/test/myjar.jar)

其他JAR位于myjar.jar(/opt/test/lib/mylib.jar)相同路径下的目录中。

该库已注册到myjar.jarMETA-INF/MANIFEST.MF,文本为

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

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

然后我将设置文件夹,启动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`]

所以类加载是在JAR删除之后发生的。我不明白这为什么管用。 并且以前未从lib/mylib.jar加载任何其他类。

使用的JDK为OpenJDK Runtime Environment corretto-8.302.08.1(Build 1.8.0_302-B08)

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

有人知道这种行为吗?

P。我用真正的JAR和类测试了这个过程。如果没有人知道原因,我可以构建一个测试项目。


解决方案

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

类路径上的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应用程序写入的内容。这演示了当一些数据已经被读取并且应用程序尝试根据它从旧版本中知道的来解释新数据时,覆盖打开的文件会如何造成破坏。


这产生了一种解决方案,可以在不使已经运行的JVM崩溃的情况下更新JAR文件。首先删除旧的JAR文件,这会让JVM在将新版本复制到相同位置之前,使用已经打开的、现在是私有的旧文件运行。从系统的角度来看,您有两个不同的文件。当JVM终止时,旧的将不复存在。替换后启动的JVM将使用新版本。

相关文章