JavaのFileクラスのdeleteOnExitメソッドは当然ファイル名ごとにメモリを使う

## 概要

JavaのFileクラスに、[deleteOnExit](https://docs.oracle.com/javase/jp/21/docs/api/java.base/java/io/File.html#deleteOnExit())というメソッドがあります。

プロセス終了時に、（ベストエフォートで）ファイルを消してくれるお便利なものですが、無邪気に使うとどうなるかについて考えてみました。

### 想定読者

- Java開発者の方

## 前提

### 仕様

deleteOnExitメソッドは、プロセスの終了時、ベストエフォートで、deleteOnExit()が実行されたfileを削除しようとします。

### 実装

実装は、Javaの標準クラスと、ランタイムの実装、実行環境の制約等によります。必ず消せるという保証があるものではありません。

## 実装の一つを眺めてみる

ライブラリの実装例を眺めてみます。

https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/io/File.java

によれば、DeleteOnExitHookクラスのaddメソッドに対して、削除指定されたパスを送ることになっており、

```Java
    public void deleteOnExit() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkDelete(path);
        }
        if (isInvalid()) {
            return;
        }
        DeleteOnExitHook.add(path);
    }
```

呼び先のクラスを見ると、LinkedHashSetの中にファイルのパスを保持しておいて、JVMのhsutdownHookを利用し、終了時にファイルを削除しに行こうとする、という構造になっていました。

> LinkedHashSetというのがポイントで、WEB検索で見た昔の記事では、同一パスであっても利用メモリが増えていくのはいかがなものか、といったやりとりも筆者は目にしました。


```Java

class DeleteOnExitHook {
    private static LinkedHashSet<String> files = new LinkedHashSet<>();
    static {
        // DeleteOnExitHook must be the last shutdown hook to be invoked.
        // Application shutdown hooks may add the first file to the
        // delete on exit list and cause the DeleteOnExitHook to be
        // registered during shutdown in progress. So set the
        // registerShutdownInProgress parameter to true.
        SharedSecrets.getJavaLangAccess()
            .registerShutdownHook(2 /* Shutdown hook invocation order */,
                true /* register even if shutdown in progress */,
                new Runnable() {
                    public void run() {
                       runHooks();
                    }
                }
        );
    }

    private DeleteOnExitHook() {}

    static synchronized void add(String file) {
        if(files == null) {
            // DeleteOnExitHook is running. Too late to add a file
            throw new IllegalStateException("Shutdown in progress");
        }

        files.add(file);
    }
```

## この前提で何がおこるか

長時間生きているプロセスであれば、当然ながら、利用メモリが増えていきます。

### 実験

一時ファイルをたくさん作り、メモリ使用量が増えるかどうかについて実験します。



### 実験用プログラム

このような実験プログラムを作ります。

```Java
package too_many_delete_on_exit_files_test;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.concurrent.TimeUnit;

public class Main {
	public static void printMemoryUsage() {
		MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();

		MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
		long heapUsed = heapMemoryUsage.getUsed();
		long heapCommitted = heapMemoryUsage.getCommitted();

		MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
		long nonHeapUsed = nonHeapMemoryUsage.getUsed();
		long nonHeapCommitted = nonHeapMemoryUsage.getCommitted();

		System.out.println("Heap Memory Used: " + heapUsed + " bytes");
		System.out.println("Heap Memory Committed: " + heapCommitted + " bytes");
		System.out.println("Non-Heap Memory Used: " + nonHeapUsed + " bytes");
		System.out.println("Non-Heap Memory Committed: " + nonHeapCommitted + " bytes");
	}
	
	private static void sleepALittle(int sec) {
		try {
			Thread.sleep(TimeUnit.SECONDS.toMillis(sec));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}		
	}

	public static void main(String[] args) {
		long max = 100000;
		System.out.println("please invoke jconsole and attach to this process.");
		sleepALittle(30);
		System.out.println("initializing..");
		System.gc();
		sleepALittle(3);
		printMemoryUsage();
		System.out.println("start");
		try {
			for (long l = 0; l < max; l++) {
				File file = File.createTempFile("prefix", ".tmp");
				file.deleteOnExit();
				try (FileWriter fw = new FileWriter(file)) {
					fw.append("1");
				}
				if (l % 5000 == 0) {
					System.out.println("loop:" + l);
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.gc();
		sleepALittle(3);
		printMemoryUsage();

		try {
			Thread.sleep(TimeUnit.SECONDS.toMillis(60));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
```

### 結果

一時ファイルをたくさん(10万個)作ってdeleteOnExitを呼んだ前後で、メモリが増えます。

雑音要素も入るところではありますが、Heap Memory Usedのところで比較すると、ある程度傾向がつかめると思います。

```console
please invoke jconsole and attach to this process.
initializing..
Heap Memory Used: 3025120 bytes
Heap Memory Committed: 62914560 bytes
Non-Heap Memory Used: 20866336 bytes
Non-Heap Memory Committed: 25493504 bytes
start
loop:0
loop:5000
loop:10000
loop:15000
loop:20000
loop:25000
loop:30000
loop:35000
loop:40000
loop:45000
loop:50000
loop:55000
loop:60000
loop:65000
loop:70000
loop:75000
loop:80000
loop:85000
loop:90000
loop:95000
Heap Memory Used: 20079496 bytes
Heap Memory Committed: 62914560 bytes
Non-Heap Memory Used: 23312176 bytes
Non-Heap Memory Committed: 27328512 bytes

```

JConsoleで、実験プログラムの実行に関するメモリの利用量を確認すると、下図のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65002/11e32217-8fb3-3635-f673-577ef99246f4.png)

## おわりに

この記事では、Javaの標準ライブラリのうち、FileクラスのdeleteOnExit()メソッドを使った場合、結果として何が起きるかについて扱いました。

テンポラリなファイルを作るうえでは便利なメソッドですが、どんな場合でも必ず消えることが期待できるわけではない点と、プロセスが終了するまでメモリに残る実装となっている場合がある点については、注意をしたうえでの利用が必要だと思いました。