Почему у объекта, созданного ClassLoader, нет возможности собирать мусор

Я имею в виду этот пример кода, который сообщается в http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6254531

import java.net.URL;

class Loader {
    public static void main(String[] args) throws Exception {
        for (;;) {
            System.gc();
            System.out.print(".");
            System.out.flush();
            new java.net.URLClassLoader(
                new URL[] { new java.io.File(".").toURL() },
                ClassLoader.getSystemClassLoader().getParent()
            ).loadClass("Weakling").newInstance();
        }
    }
}
public class Weakling {
    private static ThreadLocal<Object> local;
    private static Weakling staticRef;
    private Object var = new byte[1000*1000];
    public Weakling() {
        local = new ThreadLocal<Object>();
        local.set(this);
        staticRef = this;
    }

    @Override
    protected void finalize() {
        System.out.print("F");
        System.out.flush();
    }
}

Финализация никогда не будет вызываться. Однако, если я изменю

            new java.net.URLClassLoader(
                new URL[] { new java.io.File(".").toURL() },
                ClassLoader.getSystemClassLoader().getParent()
            ).loadClass("Weakling").newInstance();

к

new Weakling();

Он работает очень хорошо и не обнаруживается утечка.

Может ли кто-нибудь объяснить, почему у объекта, созданного ClassLoader, нет возможности собирать мусор?

Ответы

Ответ 1

Механизм ThreadLocal эффективно сохраняет в текущем потоке значение WeakHashMap экземпляров ThreadLocal для значений. Следовательно, если экземпляр ThreadLocal никогда не становится слабо ссылочным, тогда запись эффективно протекает.

Можно рассмотреть два случая. Для простоты обсуждения предположим, что ThreadLocal фактически хранит WeakHashMap в Thread.currentThread(); в действительности он использует более сложный механизм, который имеет эквивалентный эффект.

Сначала рассмотрим сценарий "нового ослабления":

  • На первой итерации цикла:
    • Класс Weackling загружается из загрузчика системного класса
    • Конструктор Weakling называется
    • Статическая переменная Weakling.local устанавливается с нуля на новый экземпляр ThreadLocal # 1
    • ThreadLocal WeakHashMap обновляется для хранения нового экземпляра Weackling # 1
  • Во всех последующих итерациях цикла:
    • Класс Weakling уже загружен из загрузчика системного класса
    • Конструктор Weakling называется
    • Статическая переменная Weakling.local устанавливается из старого экземпляра ThreadLocal # 1 в новый экземпляр ThreadLocal # 2. Старый экземпляр ThreadLocal # 1 теперь только (слабо) ссылается на WeakHashMap.
    • ThreadLocal WeakHashMap обновляется для хранения нового экземпляра Weakling. Во время этой операции WeakHashMap замечает, что старый экземпляр ThreadLocal № 1 слабо ссылоспособен, поэтому он удаляет запись [ThreadLocal instance # 1, Weakling # 1] с карты, прежде чем добавит [ThreadLocal instance # 2, Weakling # 2 ].

Во-вторых, рассмотрим новый сценарий URLClassLoader (...). loadClass (...). newInstance():

  • На первой итерации цикла:
    • Класс Weackling # 1 загружается из URLClassLoader # 1
    • Конструктор Weakling называется
    • Статическая переменная Weakling.local # 1 устанавливается от нуля до нового экземпляра ThreadLocal # 1
    • ThreadLocal WeakHashMap обновляется для хранения нового экземпляра Weackling # 1
  • На всех последующих итерациях цикла
    • Класс Wekling #n загружается из URLClassLoader #n
    • Конструктор Weakling называется
    • Статическая переменная Weakling.local #n устанавливается из нуля в новый экземпляр ThreadLocal #n
    • ThreadLocal WeakHashMap обновляется для хранения нового экземпляра Weakling.

Обратите внимание, что на этом последнем этапе экземпляр ThreadLocal # 1 не является ссылочным. Это происходит из-за следующей ссылочной цепочки:

  • Значение WeakHashMap сильно ссылается на экземпляр Weakling # 1
  • Условный экземпляр # 1 сильно ссылается на класс Weakling # 1 через Object.getClass()
  • Класс ослабления # 1 сильно ссылается на экземпляр Thread1ocal # 1 через переменную статического класса

Пока цикл продолжает работать, в ThreadLocal WeakHashMap добавляется больше записей, а сильная ссылочная цепочка от значения к ключу (экземпляр Weakling в ThreadLocal) в WeakHashMap предотвращает сбор мусора в других устаревших записях.

Я изменил программу Loader итерации 3 раза, а затем дождался ввода пользователя. Затем я сгенерировал кучу кучи, используя java -Xrunhprof: heap = dump и ctrl-pause/break. Ниже приведен мой анализ окончательного сброса кучи:

Во-первых, есть три объекта Weakling:

OBJ 500002a1 (sz=16, trace=300345, [email protected])
OBJ 500003a4 (sz=16, trace=300348, [email protected])
OBJ 500003e0 (sz=16, trace=300342, [email protected])

Обратите внимание, что все три экземпляра Weakling (500002a1, 500003a4 и 500003e0) создаются из трех отдельных экземпляров класса (50000296, 5000039d и 500003d9 соответственно). Посмотрев на первый объект, мы видим, что он хранится как значение в объекте записи на карте threadLocal:

OBJ 500002a5 (sz=32, trace=300012, [email protected])
        referent        500002a4
        queue           500009f6
        value           500002a1

Референтом здесь является слабое значение:

OBJ 500002a4 (sz=16, trace=300347, [email protected])

Поиск, мы видим, что этот объект хранится как значение в статическом переменная "локальная" из вышеупомянутого класса Славянства:

CLS 50000296 (name=Weakling, trace=300280)
        super           50000099
        loader          5000017e
        domain          50000289
        static local    500002a4
        static staticRef        500002a1

В заключение мы имеем следующую сильную цепочку опорных цепей для этого экземпляра Weakling, который мешает ему собирать мусор.

  • Значение WeakHashMap (500002a5) сильно ссылается на экземпляр Weakling (500002a1)
  • Условный экземпляр (500002a1) сильно ссылается на класс Weakling (50000296) через Object.getClass()
  • Класс ослабления (50000296) сильно ссылается на экземпляр ThreadLocal (500002a4) через переменную статического класса

Аналогичный анализ на других объектах Славянства показал бы аналогичный результат. Разрешить запуск программы для дополнительных итераций показывает, что объекты продолжают накапливаться таким образом.