1

I am currently busy with building a simple javafx application. In this application, you can add and remove .jar files from within the application, from which I am gathering some information. One of the things I use from these .jar files are textures stored as images.

Whenever I load an Image using a path from the jar file, I am unable to remove this .jar file later on. This is quite logical as the .jar file is now being used by this Image, but for some reason, I am unable to delete the .jar file from within the application even when removing all references to this Image and calling the garbage collector.

The way I am currently trying to delete the file is the following:

File file = new File("mods/file.jar");
file.delete();

At some point in the application I initialize the image as follows:

Image block = new Image(pathToJar);

I have tried the following things to resolve my problem:

  • Set block = null; and call System.gc();
  • Call Image.cancel() and call System.gc();
  • I have also tried the above in combination with System.runFinalization();
  • I tried removing the .jar file when the stage is closing in hopes of everything being unloaded, but without success.

At this point in time, I am not quite sure why I am unable to actually delete the .jar file. Could anyone possibly explain to me why this is the case and how it could be resolved? Many thanks in advance!

EDIT: the purpose of the application was not clear enough. I am making a color picker that takes average colors of textures from .jar files (Minecraft mods). These .jar files are to be added and deleted from the application in a flexible way. Once you do not want to consider textures of a certain .jar file anymore, you just remove it from the application and be done with it. For ease of use, I made copies of the .jar files so I can access them relatively. Once you are done with the .jar file I want to remove this copy as it will just take in unnecessary storage.

I have narrowed the problem (thus far) down to the initialization of an Image. On request, here a MRE:

public class example extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        // Read image, needed at some point in my code
        Image block = new Image("jar:File:./mods/botania.jar!/assets/botania/textures/blocks/alfheim_portal.png");

        // Make it so that jar is not used anymore
        block.cancel();
        block = null;
        System.gc();

        // Try to delete the file after being done with it
        File delete = new File("mods/botania.jar");
        Files.delete(delete.toPath());
    }
}

After initializing the image, and thereafter removing references to it, it should be cleaned by the garbage collector (at least to my knowledge). Instead, now it just gives the following error:

Caused by: java.nio.file.FileSystemException: mods\botania.jar: The process cannot access the file because it is being used by another process.

G. de Man
  • 149
  • 9
  • 1
    [mcve] please .. you can't change the jar (which is a zip), not without some tooling that expands and re-zips it later on – kleopatra Jul 21 '20 at 11:35
  • hmm .. might be misunderstanding what you want, trying again: you have a jar (zip) which is unrelated (?) to your application other than containing image/s that you extract from it, then want to delete that unrelated jar (zip)? If so, it should be possible to delete the jar (my expectation). Anyway, we'll still need that reproducible example. – kleopatra Jul 21 '20 at 11:50
  • Try replacing file.delete() with `Files.delete(file.toPath())`. It may not be successful, but you will have an informative exception, whose full stack trace you should edit into your question. – VGR Jul 21 '20 at 11:56
  • @kleopatra I added an MRE and the thrown Exception to the code as requested. Thanks for informing me about these issues. – G. de Man Jul 21 '20 at 12:09
  • Just guessing here: I *think* (but not certain) that the `"jar:...` URL being passed to the `Image` constructor is going to result in a new `ClassLoader` being created to load resources from that jar file. That class loader is likely set up as a child of the system class loader, so I see a reference being retained to it, etc. I'd use the `java.util.jar` API to get an input stream from the relevant jar entry, and use the `Image` constructor taking the input stream. – James_D Jul 21 '20 at 12:31
  • 1
    Which os is this? – matt Jul 21 '20 at 12:51
  • Interestingly, I can't reproduce this, even without the calls to `cancel()`, nulling the reference out, and calling `System.gc()`, I can delete the jar file (and even still display the image in an `ImageView`). Running on OS X, JDK14, JavaFX 14. I'd still recommend using the `java.util.jar` API though. – James_D Jul 21 '20 at 12:51
  • @James_D very interesting! I also tried using an inputStream using the `java.util.jar` API, but without success. Running Windows 10, JDK8, JavaFX 11. – G. de Man Jul 21 '20 at 13:10
  • Doesn't JavaFX 11 require at least JDK11? – James_D Jul 21 '20 at 13:11
  • 3
    Windows likes to hang onto file handles. It sounds like it is a caching when using the "jar:" url. https://stackoverflow.com/a/54777849/2067492 – matt Jul 21 '20 at 13:15
  • @matt nice! That indeed seems to fix the problem. Very curious (and maybe slightly annoying). I will mark this as a duplicate with that thread. Thanks for your help everybody! – G. de Man Jul 21 '20 at 13:24
  • Though that thread (assuming I understand it correctly) seems to indicate the issue is that the internal handling of the URL doesn't close the `JarFile` (if caching is enabled). So using the `java.util.jar` API should also work, as long as you ensure you close it. – James_D Jul 21 '20 at 13:42
  • Verified the `java.util.jar` API approach works on both windows and Mac. Updated answer accordingly. – James_D Jul 21 '20 at 14:19

1 Answers1

4

I can't reproduce the issue on my system (Java 14 on Mac OS X); however running it on Windows 10 with the same JDK/JavaFX versions produces the exception you describe.

The issue appears to be (see https://stackoverflow.com/a/54777849/2067492, and hat-tip to @matt for digging that out) that by default the URL handler for a jar: URL caches access to the underlying JarFile object (and doesn't call close(), even if an InputStream obtained from it is closed). Since Windows holds on to file handles in those situations, the underlying OS is unable to delete the jar file.

I think the most natural way to address this is to use the java.util.jar API (or just the plain java.util.zip API), which will give you far more control over closing resources than generating a URL and passing the URL to the Image constructor.

Here's a self-contained example using this approach, which works both on my Mac and on my Windows 10 virtual VM:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import javax.imageio.ImageIO;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class App extends Application {
    
    @Override
    public void init() throws Exception {
        // create an image in a jar file:
        
        BufferedImage image = new BufferedImage(100, 100, ColorSpace.TYPE_RGB);
        Graphics graphics = image.getGraphics();
        graphics.setColor(new Color(0xd5, 0x5e, 0x00));
        graphics.fillRect(0, 0, 100, 100);
        graphics.setColor(new Color(0x35, 0x9b, 0x73));
        graphics.fillOval(25, 25, 50, 50);

        JarOutputStream out = new JarOutputStream(new FileOutputStream("test.jar"));
        ZipEntry entry = new ZipEntry("test.png");
        out.putNextEntry(entry);
        ImageIO.write(image, "png", out);
        out.close();
    }

    @Override
    public void start(Stage stage) throws Exception {
                
        assert Files.exists(Paths.get("test.jar")) : "Jar file not created";

        JarFile jarFile = new JarFile("test.jar");
        JarEntry imgEntry = jarFile.getJarEntry("test.png");
        InputStream inputStream = jarFile.getInputStream(imgEntry);
        Image img = new Image(inputStream);
        jarFile.close();
        
//      The following line (instead of the previous five lines) works on Mac OS X, 
//      but not on Windows
//      
//      Image img = new Image("jar:file:test.jar!/test.png");
        
        Files.delete(Paths.get("test.jar"));
        
        assert ! Files.exists(Paths.get("test.jar")) : "Jar file not deleted";
        
        ImageView iview = new ImageView(img);
        Scene scene = new Scene(new BorderPane(iview), 200, 200);
        stage.setScene(scene);
        stage.show();
        
    }

    public static void main(String[] args) {
        launch();
    }

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Yes, this also worked! I had a small mistake in my own implementation that still prevented the jar file to be deleted. Thanks again :) – G. de Man Jul 21 '20 at 14:44