[英]Java: after jar is removed, class is still loaded
I was trying to reproduce a bug where, a jar is updated (via rsync on a linux box) and then a NoClassDefFoundError
was thrown.我试图重现一个错误,其中更新了 jar(通过 linux 框上的 rsync),然后引发了
NoClassDefFoundError
。 The updated jar was unchanged, but I was thinking about the fact the file was transferring while the class was loading...更新后的 jar 没有改变,但我在考虑在 class 加载时文件正在传输的事实......
I'm now trying to reproduce the bug.我现在正在尝试重现该错误。
My application start with a classpath of only one jar ( /opt/test/myjar.jar
)我的应用程序的类路径只有一个 jar (
/opt/test/myjar.jar
)
The others jar are inside a directory on the same path of myjar.jar ( /opt/test/lib/mylib.jar
).其他 jar 位于 myjar.jar (
/opt/test/lib/mylib.jar
) 同一路径上的目录中。
The library is registered into the myjar.jar
META-INF/MANIFEST.MF
with this text该库已注册到
myjar.jar
META-INF/MANIFEST.MF
与此文本
Manifest-Version: 1.0
Built-By: FB
Class-Path: lib/mylib.jar
Now I scripted some code for waiting some seconds, then load some class with Class.forName("mylib.MyClass")
.现在我编写了一些代码等待几秒钟,然后用
Class.forName("mylib.MyClass")
加载一些 class 。
Then I will setup the folder, start the java runtime and then delete the lib/mylib.jar
file, and wait for the Class.forName
to fail.然后我将设置文件夹,启动 java 运行时,然后删除
lib/mylib.jar
文件,并等待Class.forName
失败。
And the code was running fine.并且代码运行良好。 I was expecting a
NoClassDefFoundError
.我期待一个
NoClassDefFoundError
。 Then I rerun the code, and a NoClassDefFoundError
was thrown.然后我重新运行代码,抛出了
NoClassDefFoundError
。
Then I readded the mylib.jar
to the lib
directory, rerun, all ok.然后我将
mylib.jar
入lib
目录,重新运行,一切OK。
Then I rerun the code with -verbose:class
, deleted the lib/mylib.jar
and then this log appeared.然后我用
-verbose:class
重新运行代码,删除了lib/mylib.jar
然后出现了这个日志。
[Loaded mylib.MyClass from file:/opt/test/lib/mylib.jar`]
So the class loading was happening after the jar deletion.所以 class 加载发生在 jar 删除之后。 I don't understand why this work.
我不明白为什么会这样。 And no other classes were loaded from
lib/mylib.jar
before.并且之前没有从
lib/mylib.jar
加载其他类。
Jdk used is OpenJDK Runtime Environment Corretto-8.302.08.1 (build 1.8.0_302-b08)使用的 jdk 是 OpenJDK Runtime Environment Corretto-8.302.08.1 (build 1.8.0_302-b08)
I don't understand how the JVM can load a class from a file I just deleted.我不明白 JVM 如何从我刚刚删除的文件中加载 class 。 I think maybe the JVM cache those files somewhere (mabye cause they are registered inside the
MANIFEST.MF
).我想也许 JVM 将这些文件缓存在某处(可能是因为它们已在
MANIFEST.MF
中注册)。
Have anyone an idea of this behavior??有人知道这种行为吗?
Ps.附言。 I tested this exact procedure, but with real jar and classes.
我测试了这个确切的过程,但使用了真正的 jar 和类。 If no one have an idea on the why, I can build an test project.
如果没有人知道原因,我可以构建一个测试项目。
You are using a system without mandatory file locking.您正在使用没有强制文件锁定的系统。 If you tried the same under Windows, for example, you couldn't do neither, overwrite nor delete the.jar file.
例如,如果您在 Windows 下尝试了相同的操作,则既不能覆盖也不能删除 .jar 文件。
The jar files on the class path are opened when the JVM starts and kept open during the runtime. JVM 启动时打开 class 路径上的 jar 文件,并在运行时保持打开状态。 We can demonstrate the behavior using ordinary file operations:
我们可以使用普通的文件操作来演示该行为:
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);
}
prints something like打印类似的东西
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
demonstrating that after removing, we can still write and read data from the already opened file, as only the entry has been removed from the directory.证明删除后,我们仍然可以从已打开的文件中写入和读取数据,因为只有条目已从目录中删除。 The JVM is now operating on a file without a name.
JVM 现在正在对没有名称的文件进行操作。 But as soon as this filehandle has been closed, trying to open it again will fail, as now it is really gone.
但是一旦这个文件句柄被关闭,试图再次打开它就会失败,因为现在它真的消失了。
Overwriting the file, however, is a different thing.然而,覆盖文件是另一回事。 When opening the existing file, we access the same file and make changes perceivable.
打开现有文件时,我们访问同一个文件并使更改变得可感知。
So所以
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));
}
produces something like产生类似的东西
opened /home/tux/test7100435925076742504.tmp
wrote 9 bytes into /home/tux/test7100435925076742504.tmp
cp ran with rc 0
read 9 bytes, cp/proc/
Showing that the read
operation on the already open file resulted in what cp
wrote, partially of course, as the buffer was pre-sized to what the Java application wrote.表明对已打开文件的
read
操作导致了cp
写入的内容,当然,部分原因是缓冲区的大小已预先设置为 Java 应用程序写入的内容。 This demonstrates how overwriting an open file can cause havoc when some data has been read already and the application tries to interpret the new data according to what it knows from the old version.这演示了当一些数据已经被读取并且应用程序尝试根据它从旧版本中知道的内容来解释新数据时,覆盖打开的文件会如何造成破坏。
This leads to a solution for updating a jar file without crashing the already running JVM.这导致了一种更新 jar 文件的解决方案,而不会使已经运行的 JVM 崩溃。 Delete the old jar file first, which lets the JVM run with the already opened, now-private old file, before copying the new version to the same location.
首先删除旧的 jar 文件,这让 JVM 可以使用已经打开的、现在为私有的旧文件运行,然后将新版本复制到同一位置。 From the system's perspective, you have two different files then.
从系统的角度来看,您有两个不同的文件。 The old one will cease to exist when the JVM terminates.
当 JVM 终止时,旧的将不复存在。 JVMs started after the replacement will use the new version.
替换后启动的 JVM 将使用新版本。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.