finalize и Finalizer

в 8:02, , рубрики: finalize, finalizer, java, метки: , ,

Сегодня немного поэкспериментируем с методом finalize() и уничтожением объектов. Хотя даже начинающие Java-программисты примерно представляют, что finalize() вызывается, когда сборщик мусора решит уничтожить ваш объект, некоторые вещи могут всё-таки оказаться неожиданными. К примеру, зададимся вопросом: что случится с вашим приложением, если метод finalize() работает очень долго?

В официальной документации о finalize() сказано следующее:

Called by the garbage collector on an object when garbage collection determines that there are no more references to the object.

Из этого можно предположить, что «зависший» finalize() подвесит поток сборщика мусора, и сборка прекратится. На самом деле (по крайней мере, в HotSpot 1.6) сборщик мусора не вызывает методы finalize() напрямую, а только добавляет соответствующие объекты в специальный список, вызывая статический метод java.lang.ref.Finalizer.register(Object). Объект класса Finalizer представляет собой ссылку на объект, для которого надо вызвать finalize(), и хранит ссылки на следующий и предыдущий Finalizer, формируя двусвязный список.

Непосредственно вызов finalize() происходит в отдельном потоке «Finalizer» (java.lang.ref.Finalizer.FinalizerThread), который создаётся при запуске виртуальной машины (точнее в статической секции при загрузке класса Finalizer). Методы finalize() вызываются последовательно в том порядке, в котором были добавлены в список сборщиком мусора. Соответственно, если какой-то finalize() зависнет, он подвесит поток «Finalizer», но не сборщик мусора. Это в частности означает, что объекты, не имеющие метода finalize(), будут исправно удаляться, а вот имеющие будут добавляться в очередь, пока не отвиснет поток «Finalizer», не завершится приложение или не кончится память.

Проиллюстрируем это на примере. Создадим класс, объекты которого съедают прилично места в куче:

    static class BigObject {
        char[] tmp = new char[10000];
    }

И напишем примерно такой метод main:

    public static void main(String... args) {
        int i=0;
        while(true) {
            new BigObject();
            try {
                Thread.sleep(10);
            }
            catch( InterruptedException e ) {}
            if(i++%100==0)
                System.out.println("Total: "+Runtime.getRuntime().totalMemory()+
                        "; free: "+Runtime.getRuntime().freeMemory());
        }
    }

Создаём по объекту на каждой итерации, а раз в сто итераций выводим информацию об оставшейся памяти. Ограничим память Java-машины, чтобы не затягивать тесты, и посмотрим результат:
$ java -Xms16m -Xmx16m Test
Total: 16252928; free: 15965064
Total: 16252928; free: 14013136
Total: 16252928; free: 12011536
Total: 16252928; free: 14309664
Total: 16252928; free: 12308064
Total: 16252928; free: 14797440
Total: 16252928; free: 12795840
Total: 16252928; free: 15307784
...

Память исправно выделяется и освобождается.

Теперь создадим класс, объекты которого выполняют finalize() очень долго:

    static class LongFinalize {
        protected void finalize() throws Throwable {
            System.out.println("LongFinalize finalizer");
            Thread.sleep(10000000);
        }
    }

Добавим new LongFinalize() в main() перед циклом. Результат будет таким:
$ java -Xms16m -Xmx16m Test
Total: 16252928; free: 15965064
Total: 16252928; free: 14003496
Total: 16252928; free: 12001896
LongFinalize finalizer
Total: 16252928; free: 14290408
Total: 16252928; free: 12288808
Total: 16252928; free: 14777432
Total: 16252928; free: 12775832
Total: 16252928; free: 15286960
Total: 16252928; free: 13280880

Как видно, несмотря на вызов LongFinalize.finalize() сборщик мусора продолжает работать. Теперь добавим в объект BigObject свой метод finalize(), который делает что-нибудь незначительное:

    static class BigObject {
        char[] tmp = new char[10000];
        protected void finalize() throws Throwable {
            tmp[0] = 1;
        }
    }

На этот раз картина иная:
$ java -Xms16m -Xmx16m Test
Total: 16252928; free: 15965064
Total: 16252928; free: 14003496
Total: 16252928; free: 12001896
LongFinalize finalizer
Total: 16252928; free: 9996648
Total: 16252928; free: 7987144
Total: 16252928; free: 6459728
Total: 16252928; free: 4458128
Total: 16252928; free: 6357016
Total: 16252928; free: 4347352
Total: 16252928; free: 2331112
Total: 16252928; free: 329512
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Test$BigObject.<init>(Test.java:12)
at Test.main(Test.java:31)

Заметим, что один раз памяти всё же стало больше: уничтожились те объекты BigObject, для которых finalize() успел выполниться до LongFinalize.finalizer().

Выше я написал, что исправно удаляются в таких условиях только объекты, не имеющие finalize(). На самом деле достаточно, чтобы метод finalize() был пустым. Сборщик мусора добавляет объект в очередь Finalizer только при наличии кода в теле finalize(). К примеру, мы можем создать дочерний класс с пустым finalize():

    static class SubBigObject extends BigObject {
        protected void finalize() throws Throwable {
        }        
    }

И создавать в main() объекты дочернего класса (заменить new BigObject() на new SubBigObject()). Мы увидим, что сборка мусора снова идёт успешно.

Таким образом, вы можете ускорить уничтожение объектов и даже защититься от зависшего потока Finalizer, если сделаете подкласс с пустым finalize() и будете создавать только дочерние объекты. Разумеется, вы должны отдавать себе отчёт в том, что делаете: если finalize() был написан, наверно, он для чего-то был нужен. И всё же не пишите finalize() без крайней нужды. К примеру, казалось бы в абстрактном классе InputStream можно было сделать finalize(), вызывающий close() для пущего удобства. По факту же finalize() определён только в тех дочерних классах, которые работают непосредственно с системными ресурсами (например, FileInputStream). А, скажем, в BufferedInputStream finalize() не нужен, даже если он оборачивает FileInputStream. Здесь избыточная универсальность вредна. Если же автор какой-то библиотеки по недомыслию сделал ненужный finalize() в абстрактном классе, а вы с системными ресурсами не работаете, переопределите его с пустым телом в своей реализации. Ведь даже если Finalizer не завис, он может просто не справляться с потоком освобождаемых объектов, что приведёт к существенному замедлению их удаления и разрастанию кучи.

Следует ещё сказать о такой штуке, как System.runFinalization(). Этот вызов создаёт второй поток «SecondaryFinalizer», который так же вызывает finalize() для объектов из той же очереди. При этом поток, вызвавший System.runFinalization() ждёт, пока не кончится очередь Finalizer, которая имеется на данный момент. В принципе, он может вас спасти от OutOfMemory, если основной Finalizer завис. Вернёмся к версии программы без SubBigObject и добавим этот вызов, если памяти остаётся мало. Чтобы вы не запутались, я приведу полный текст:

public final class Test {
    static class LongFinalize {
        protected void finalize() throws Throwable {
            System.out.println("LongFinalize finalizer");
            Thread.sleep(10000000);
        }
    }
    
    static class BigObject {
        char[] tmp = new char[10000];
        
        protected void finalize() throws Throwable {
            tmp[0] = 1;
        }
    }
    
    public static void main(String... args) {
        int i=0;
        new LongFinalize();
        while(true) {
            new BigObject();
            try {
                Thread.sleep(10);
            }
            catch( InterruptedException e ) {}
            if(i++%100==0)
                System.out.println("Total: "+Runtime.getRuntime().totalMemory()+
                        "; free: "+Runtime.getRuntime().freeMemory());
            if(Runtime.getRuntime().freeMemory()<1e6) System.runFinalization();
        }
    }
}

Посмотрим на результат работы:
$ java -Xms16m -Xmx16m Test
Total: 16252928; free: 15965064
Total: 16252928; free: 14003496
Total: 16252928; free: 12001896
LongFinalize finalizer
Total: 16252928; free: 9996648
Total: 16252928; free: 7987144
Total: 16252928; free: 6459832
Total: 16252928; free: 4458232
Total: 16252928; free: 6357120
Total: 16252928; free: 4347456
Total: 16252928; free: 2331216
Total: 16252928; free: 239072
Total: 16252928; free: 11729800
Total: 16252928; free: 9717584
Total: 16252928; free: 7719416
Total: 16252928; free: 5710768
Total: 16252928; free: 3721880
Total: 16252928; free: 1710824
Total: 16252928; free: 11261488

Программа продолжает жить, несмотря на то что основной Finalizer() висит. Конечно, это вас не спасёт, если в очереди будет много объектов с долгим finalize(), и вообще явный вызов System.runFinalization() в программе скорее говорит о том, что что-то не так.

Автор: lany


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js