finalize и Finalizer

    Сегодня немного поэкспериментируем с методом 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() в программе скорее говорит о том, что что-то не так.
    Поделиться публикацией

    Похожие публикации

    Комментарии 26

      +4
      Как вы считаете, вообще пользоваться finalize имеет смысл? Ведь это ставит логику приложения в зависимость от работы сборщика мусора. Разве это правильно?
        +1
        Имеет смысл всегда явно закрывать ресурсы, не полагаясь на finalize вообще. В Java 1.7 для этого даже есть удобная штука try-with-resources. В принципе, конечно, можно вообще отказаться от finalize, надеясь на добросовестность разработчика. Другое дело, что многие библиотеки (в том числе системные) его всё же используют, и с этим приходится считаться.
          +3
          Есть особый сценарий использования, когда вы имеете объекты, связанные через WeakReference/SoftReference. Скажем, объект, достающий информацию из какого-то большого файла (так что весь файл загрузить и закрыть его нельзя). Объект держит файл открытым, чтобы при запросах быстро спозиционироваться и достать нужную запись, но при этом при нехватке памяти вы не возражаете, если он будет уничтожен (но файл надо закрыть). При следующем обращении к файлу вы просто снова создадите такой объект. Если отказаться от finalize(), с такого рода объектами жизнь станет сложнее: придётся отслеживать ReferenceQueue, возиться с фантомными ссылками и т. д.
            0
            Спасибо, интересный подход. Вообще я программирую на AS3 и у нас нет такого метода, а слабые ссылки есть, поэтому в отсутствии finalize слабыми ссылками я тоже не пользуюсь.
              0

              Вообще-то, при таком сценарии файл будет закрыт когда файловый поток будет собран сборщиком мусора. То есть не требуются ни метод finalize, ни фантомные ссылки, ни ReferenceQueue.

                0

                Естественно. Только сборщик мусора срабатывает, когда памяти мало, ему плевать, если у вас мало файл-дескрипторов (нередка ситуация, когда процесс не может открыть, например, больше 1000 файлов, потому что ulimit так настроен). Это бомба замедленного действия, когда вы полагаетесь на сборщик мусора с открытыми файлами. Когда она выстрелит у каких-нибудь клиентов, вы поймёте, что чтобы исправить эту проблему, переделывать придётся слишком много.

                  0

                  Нет никакой разницы между


                  private FileInputStream file;
                  
                  protected void finalize() throws Throwable {
                      file.close();
                  }

                  и тем же самым кодом, но без finalize(). Когда объект уже собран сборщиком мусора — закрывать управляемые ресурсы поздно!

                    0

                    Я абсолютно не понимаю, зачем вы выделили "уже" курсивом и полужирным и как это относится к моему комментарию. А разница определённо есть: здесь вам потребуется три цикла сборки мусора, чтобы всё прибрать. А без finalize() только два.

                      0

                      То есть вы тоже согласны, что finalize() тут лишний? Тогда как вы собрались с помощью finalize делать кеширующий читатель большого файла?

                        0

                        Под "отказаться от finalize" подразумевалось не использовать его в том числе в объекте, который непосредственно держит ресурс (например, FileInputStream). Хотя это было писано четыре года назад. Сейчас PhantomReference даже в данном случае выглядит привлекательнее. Тем более, в Java 9 появится публичный Cleaner, который гораздо лучше finalize.

              0
              Иногда проще подержать некритичные ресурсы открытыми, чем реализовывать подсчет ссылок
              +2
              В одном крупном и достаточно популярном open-source проекте встречал следующее:

              protected void finalize() throws Throwable {
                  this.rows = null;
                  this.tables = null;
                  this.frame = null;
                  ....
              }
              


              Т.е. программист сознательно «занулял» все поля объекта. Сдается мне в корпоративных приложениях на вроде «Система внутреннего документа оборота ТраснКредитСельхозУнипроЛевобережного Банка» таких вещей оч. много.
                –4
                В этом нет ничего плохого, т.к. в определенных ситуациях помогает GC быстрее определять наличие ссылок на объект.
                  +1
                  Ценой того, что родительский объект удаляется не сразу, а в два прохода, между которыми надо отстоять очередь Finalizer'а? Это может быть оправдано для стековых переменных, но в данном случае — очень сомневаюсь.
                    +3
                    Конечно, в этом нет ничего плохого — это полный п ужас!
                    1. Никогда, никогда не помогайте GC (особенно через finalize), только хуже сделаете.
                    2. Если возможно (т.е. вы не завязаны на legacy code) — не юзайте finalize(), мы кричим об этом годами, но никто не слышит.
                    3. Если все-таки необходимо завязываться на освобождение ресурсов — только <*>Reference. Не забудьте, * — подставить Soft/Weak/Phantom — по необходимости.
                      0
                      Я наверно не так выразился. Нет ни чего плохого не в финализаторе.
                        +1
                        Есть.
                        Финализатор — это плохо.
                        Финализатор «помогающий GC» — это еще хуже.
                  +3
                  PhantomReferences?
                    +1
                    Не понимаю, за что этому коменту поставили минус и, к сожалению, не могу это поправить. Но ведь человек абсолютно прав — PhantomReference были придуманы как альтернатива finalize. И если finalize требует каких-то длительных вычислений, то рекомендуется переписать логику с использованием PhantomReference — в таком случае можно будет самому следить за очередью объектов на очистку и самому выполнять чистку в отдельном пуле потоков.
                      0
                      Наверное, минус поставили те, кто думает, что PhantomReference – это такая неудачная шутка. Однако, фантомы вполне реальны.

                      Немного разъясню вашу мысль. Давайте сделаем вот так:
                      ReferenceQueue queue = new ReferenceQueue();
                      PhantomReference ref = new PhantomReference(object, queue);
                      

                      После того, как object будет уже никому не нужен он добавится в queue. Его можно будет забрать вот так:
                      PhantomReference ref = queue.remove();
                      

                      Понятно, что забирать и обрабатывать эти ссылки можно в своей нити. Стоит отметить, что ref.get() всегда возвращает null, хотя object и жив внутри приватного поля ссылки.

                      Этот механизм позиционируется как более гибкий, чем finalize(). Хотя, если честно, когда я вижу такие кренделя в неспециальных проектах у меня по коже муражки идут.
                    +3
                    Тема воскрешения объектов, помеченных на удаление, не раскрыта.
                    В finalize можно спокойно поместить код, который делает объект снова достижимым.
                    (Я вовсе не говорю, что так нужно делать. Просто ещё один аргумент против finalize)
                      0
                      Совершенно верно. Это одно из основных применений finalize. Например, когда нужно отлавливать «брошенный» ресурс, чтобы вернуть его обратно в пул.
                        +1
                        Вы слово «брошенный» специально выбрали, чтобы с ником сочлось? :)
                      0
                      Спасибо автору. Интересный прием с пустым finalize(){}; Конечно с оговорками, которые были упомянуты в посте.
                      Finalize конечно, в большом мире java достаточно редко используемая вешь, но где используется Week/SoftReference вполне возможная ситуация, когда finalize будет необходим.
                      За пост — пять!
                        0
                        Для этого есть PhantomReference.
                        0
                        Cleaners…

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое