Java не сбор памяти для сбора мусора

Я читаю очень большой файл и извлекаю некоторые небольшие части текста из каждой строки. Однако в конце операции у меня осталось очень мало памяти для работы. Похоже, что сборщик мусора не может освободить память после чтения в файле.

Мой вопрос: есть ли способ освободить эту память? Или это ошибка JVM?

Я создал SSCCE, чтобы продемонстрировать это. Он читает в файле размером 1 мб (Java из-за 16-битного кодирования) и извлекает один символ из каждой строки (~ 4000 строк, поэтому должно быть около 8 кб). В конце теста все еще используется 2 МБ!

Исходное использование памяти:

Allocated: 93847.55 kb
Free: 93357.23 kb

Сразу после чтения в файле (перед любой ручной сборкой мусора):

Allocated: 93847.55 kb
Free: 77613.45 kb (~16mb used)

Этого следует ожидать, так как программа использует много ресурсов для чтения в файле.

Однако потом я собираю мусор, но не все память освобождается:

Allocated: 93847.55 kb
Free: 91214.78 kb (~2 mb used! That the entire file!)

Я знаю, что ручное обращение к сборщику мусора не дает вам никаких гарантий (в некоторых случаях это лениво). Однако это происходило в моем более крупном приложении, где файл ест почти всю доступную память и заставляет остальную часть программы исчерпывать память, несмотря на ее необходимость. Этот пример подтверждает мое подозрение, что лишние данные, считанные из файла, не освобождаются.

Здесь SSCCE генерирует тест:

import java.io.*;
import java.util.*;

public class Test {
    public static void main(String[] args) throws Throwable {
        Runtime rt = Runtime.getRuntime();

        double alloc = rt.totalMemory()/1000.0;
        double free = rt.freeMemory()/1000.0;

        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);

        Scanner in = new Scanner(new File("my_file.txt"));
        ArrayList<String> al = new ArrayList<String>();

        while(in.hasNextLine()) {
            String s = in.nextLine();
            al.add(s.substring(0,1)); // extracts first 1 character
        }

        alloc = rt.totalMemory()/1000.0;
        free = rt.freeMemory()/1000.0;
        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);

        in.close();
        System.gc();

        alloc = rt.totalMemory()/1000.0;
        free = rt.freeMemory()/1000.0;
        System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free);
    }
}

Ответ 1

При создании подстроки ваша подстрока сохраняет ссылку на массив char исходной строки (эта оптимизация очень быстро обрабатывает многие подстроки строки). Итак, поскольку вы сохраняете свои подстроки в списке al, вы сохраняете весь свой файл в памяти. Чтобы этого избежать, создайте новую строку, используя конструктор, который принимает строку в качестве аргумента.

Итак, в принципе, я предлагаю вам сделать

    while(in.hasNextLine()) {
        String s = in.nextLine();
        al.add(new String(s.substring(0,1))); // extracts first 1 character
    }

Исходный код конструктора String (String) явно указывает, что его использование заключается в том, чтобы обрезать "багаж":

  164       public String(String original) {
  165           int size = original.count;
  166           char[] originalValue = original.value;
  167           char[] v;
  168           if (originalValue.length > size) {
  169               // The array representing the String is bigger than the new
  170               // String itself.  Perhaps this constructor is being called
  171               // in order to trim the baggage, so make a copy of the array.
  172               int off = original.offset;
  173               v = Arrays.copyOfRange(originalValue, off, off+size);
  174           } else {
  175               // The array representing the String is the same
  176               // size as the String, so no point in making a copy.
  177               v = originalValue;
  178           }
  179           this.offset = 0;
  180           this.count = size;
  181           this.value = v;

Обновление: эта проблема исчезла с OpenJDK 7, Update 6. Люди с более новой версией не имеют проблемы.

Ответ 2

Обязательно не оставляйте ссылки, которые вам больше не нужны.

У вас все еще есть ссылки на al и in.

Попробуйте добавить al = null; in = null; перед вызовом сборщика мусора.

Кроме того, вам нужно понять, как реализуется substring. substring сохраняет исходную строку и просто использует другое смещение и длину для того же массива char[].

al.add(new String(s.substring(0,1)));

Не уверен, есть ли более элегантный способ копирования подстроки. Возможно, s.getChars() более полезен для вас.

Как и в случае с Java 8, подстрока теперь копирует символы. Вы можете убедиться, что конструктор вызывает Arrays.copyOfRange.

Ответ 3

System.gc() не является гарантией того, что JVM будет собирать мусор - это только совет JVM, который он может попробовать и собрать мусор. Поскольку имеется уже много памяти, JVM может игнорировать рекомендации и продолжать работать, пока не почувствует необходимость сделать это.

Подробнее о документации http://docs.oracle.com/javase/6/docs/api/java/lang/System.html#gc()

Другой вопрос, который говорит об этом, доступен в Когда System.gc() что-то делает