Недавно я столкнулся с более тонкой утечкой ресурсов.
Мы открываем ресурсы через getResourceAsStream загрузчика классов, и получилось так, что дескрипторы входного потока не были закрыты.
Хм, вы могли бы сказать, что за идиот.
Что делает это интересным, так это то, что таким образом вы можете просочиться в кучу памяти основного процесса, а не из кучи JVM.
Все, что вам нужно, это файл jar с файлом внутри, на который будет ссылаться код Java. Чем больше файл jar, тем быстрее выделяется память.
Вы можете легко создать такую банку с помощью следующего класса:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
Просто вставьте в файл с именем BigJarCreator.java, скомпилируйте и запустите его из командной строки:
javac BigJarCreator.java
java -cp . BigJarCreator
Et voilà: в вашем текущем рабочем каталоге вы найдете архив jar с двумя файлами внутри.
Давайте создадим второй класс:
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
Этот класс в основном ничего не делает, но создает не связанные объекты InputStream. Эти объекты будут немедленно собраны мусором и, следовательно, не влияют на размер кучи.
Для нашего примера важно загрузить существующий ресурс из файла JAR, и здесь имеет значение размер!
Если вы сомневаетесь, попробуйте скомпилировать и запустить класс выше, но обязательно выберите приличный размер кучи (2 МБ):
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
Вы не столкнетесь с ошибкой OOM здесь, поскольку ссылки не сохраняются, приложение будет работать независимо от того, насколько велико вы выбрали ITERATIONS в приведенном выше примере.
Потребление памяти вашим процессом (видимым сверху (RES / RSS) или проводником процессов) возрастает, если приложение не получает команду wait. В приведенной выше настройке он выделит около 150 МБ памяти.
Если вы хотите, чтобы приложение было безопасным, закройте поток ввода прямо там, где оно создано:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
и ваш процесс не будет превышать 35 МБ, независимо от количества итераций.
Довольно просто и удивительно.