Как использовать PhantomReference в качестве замены finalize ()

В документации Javadoc 8 для PhantomReference говорится:

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

Итак, я попытался создать поток, который вызывает метод close() тестового объекта, который подходит для сборки мусора. run() пытается получить все предварительные тестовые объекты.

Фактически все полученные тестовые объекты - это null. Ожидаемое поведение состоит в том, что тестовые объекты извлекаются и вызывается closemethod.

Независимо от того, сколько тестовых объектов вы создаете, нет ни одного тестового объекта, который можно было бы поймать pre-mortem (вам нужно увеличить таймауты и вызвать GC несколько раз).

Что я делаю неправильно? Это ошибка Java?

Запускаемый тестовый код:

Я попытался создать минимальный, полный и проверяемый пример, но он все еще довольно длинный. Я использую java version "1.8.0_121" 32-битную на 64-битной Windows 7.

public class TestPhantomReference {

    public static void main(String[] args) throws InterruptedException {
        // Create AutoClose Thread and start it
        AutoCloseThread thread = new AutoCloseThread();
        thread.start();

        // Add 10 Test Objects to the AutoClose Thread
        // Test Objects are directly eligible for GC
        for (int i = 0; i < 2; i++) {
            thread.addObject(new Test());
        }

        // Sleep 1 Second, run GC, sleep 1 Second, interrupt AutoCLose Thread
        Thread.sleep(1000);
        System.out.println("System.gc()");
        System.gc();
        Thread.sleep(1000);
        thread.interrupt();
    }

    public static class Test {
        public void close() {
            System.out.println("close()");
        }
    }

    public static class AutoCloseThread extends Thread {
        private ReferenceQueue<Test> mReferenceQueue = new ReferenceQueue<>();
        private Stack<PhantomReference<Test>> mPhantomStack = new Stack<>();

        public void addObject(Test pTest) {
            // Create PhantomReference for Test Object with Reference Queue, add Reference to Stack
            mPhantomStack.push(new PhantomReference<Test>(pTest, mReferenceQueue));
        }

        @Override
        public void run() {
            try {
                while (true) {
                    // Get PhantomReference from ReferenceQueue and get the Test Object inside
                    Test testObj = mReferenceQueue.remove().get();
                    if (null != testObj) {
                        System.out.println("Test Obj call close()");
                        testObj.close();
                    } else {
                        System.out.println("Test Obj is null");
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("Thread Interrupted");
            }
        }
    }
}

Ожидаемый результат:

System.gc()
Test Obj call close()
close()
Test Obj call close()
close()
Thread Interrupted

Фактический выход:

System.gc()
Test Obj is null
Test Obj is null
Thread Interrupted

person notes-jj    schedule 09.04.2017    source источник
comment
Test testObj = mReferenceQueue.remove().get(); всегда будет нулевым. Измените этот блок кода на mReferenceQueue.remove().close(), и он будет работать. queue.remove() будет правильно блокировать и всегда возвращать вам объект. Нет необходимости проверять null.   -  person Pacerier    schedule 19.09.2017
comment
Привет @Pacerier. mReferenceQueue.remove() вернет объект Reference<? extends Test>, а не объект Test, поэтому я не могу вызвать mReferenceQueue.remove().close(). Может быть, вы можете предоставить более подробную информацию.   -  person notes-jj    schedule 19.09.2017


Ответы (2)


Это сделано намеренно. В отличие от finalize(), который снова делает объект доступным, объекты, на которые может ссылаться только объект Reference, не могут снова стать доступными. Поэтому, когда вы собираетесь управлять ресурсом через него, вам необходимо сохранить необходимую информацию в другом объекте. Нет ничего необычного в том, чтобы использовать для этого сам объект Reference.

Рассмотрите следующие модификации вашей тестовой программы:

public class TestPhantomReference {

    public static void main(String[] args) throws InterruptedException {
        // create two Test Objects without closing them
        for (int i = 0; i < 2; i++) {
            new Test(i);
        }
        // create two Test Objects with proper resource management
        try(Test t2=new Test(2); Test t3=new Test(3)) {
            System.out.println("using Test 2 and 3");
        }

        // Sleep 1 Second, run GC, sleep 1 Second
        Thread.sleep(1000);
        System.out.println("System.gc()");
        System.gc();
        Thread.sleep(1000);
    }

    static class TestResource extends PhantomReference<Test> {
        private int id;
        private TestResource(int id, Test referent, ReferenceQueue<Test> queue) {
            super(referent, queue);
            this.id = id;
        }
        private void close() {
            System.out.println("closed "+id);
        }
    }    
    public static class Test implements AutoCloseable {
        static AutoCloseThread thread = new AutoCloseThread();
        static { thread.start(); }
        private final TestResource resource;
        Test(int id) {
            resource = thread.addObject(this, id);
        }
        public void close() {
            resource.close();
            thread.remove(resource);
        }
    }

    public static class AutoCloseThread extends Thread {
        private ReferenceQueue<Test> mReferenceQueue = new ReferenceQueue<>();
        private Set<TestResource> mPhantomStack = new HashSet<>();

        public AutoCloseThread() {
            setDaemon(true);
        }
        TestResource addObject(Test pTest, int id) {
            final TestResource rs = new TestResource(id, pTest, mReferenceQueue);
            mPhantomStack.add(rs);
            return rs;
        }
        void remove(TestResource rs) {
            mPhantomStack.remove(rs);
        }

        @Override
        public void run() {
            try {
                while (true) {
                    TestResource rs = (TestResource)mReferenceQueue.remove();
                    System.out.println(rs.id+" not properly closed, doing it now");
                    mPhantomStack.remove(rs);
                    rs.close();
                }
            } catch (InterruptedException e) {
                System.out.println("Thread Interrupted");
            }
        }
    }
}

который напечатает:

using Test 2 and 3
closed 3
closed 2
System.gc()
0 not properly closed, doing it now
closed 0
1 not properly closed, doing it now
closed 1

показывает, как использование правильной идиомы обеспечивает своевременное закрытие ресурсов и, в отличие от finalize(), объект может отказаться от посмертной очистки, что делает использование правильной идиомы еще более эффективным, поскольку в этом случае не требуется дополнительный цикл сборки мусора для восстановления объект после доработки.

person Holger    schedule 27.04.2017
comment
Не могли бы вы объяснить, почему тестовые объекты в блоке try-with-resources не добавляются в очередь ссылок? Разве они не завершены из-за каких-либо особых отношений между autocloseable и gc, о которых я не знаю? - person user35934; 19.08.2019
comment
@ user35934 помните документация по пакету: «Связь между зарегистрированным ссылочным объектом и его очередью односторонняя. То есть очередь не отслеживает ссылки, которые в ней зарегистрированы. Если зарегистрированная ссылка сама становится недоступной, она никогда не будет помещена в очередь. ». Вот почему это решение отслеживает Set<TestResource> mPhantomStack. Метод close() удаляет ресурс, делая его недоступным, что позволяет собирать его. - person Holger; 20.08.2019
comment
Вызов clear​() для ссылки также будет иметь эффект предотвращения постановки в очередь, но в любом случае он должен быть удален из глобального набора. - Обратите внимание, что логика этого решения такая же, как и в Cleaner API, который был представлен в Java 9 через несколько месяцев после этого ответа. В этом API ваш метод close() будет вызывать _ 4_, чтобы выполнить очистку и отменить регистрацию. - person Holger; 20.08.2019
comment
Метод close () удаляет ресурс, делая его недоступным, тем самым позволяя ему собираться. Это было моей мыслью, но если я закомментирую thread.remove(resource); в close()-методе, объекты с идентификаторами 2 и 3 по-прежнему не помещаются в очередь, хотя обе ссылки все еще хранятся в стеке. Вы можете попробовать это сами. - person user35934; 20.08.2019
comment
@ user35934 - это ненадежность сборки мусора. В этом конкретном случае в кадре стека есть висячие ссылки в точке, вызываемой System.gc(). У вас есть два варианта: 1) запустить пример с -Xcomp или 2) вставить, например. long dummy = 42; прямо перед строкой System.gc();. В обоих случаях это покажет, что thread.remove(resource); имеет значение. В реальных приложениях таких проблем нет. - person Holger; 20.08.2019
comment
Интересно, что в java не было возможных висячих ссылок, и что вы можете обойти это с помощью этой специальной опции командной строки. Большое спасибо за отзыв, многому научился. - person user35934; 20.08.2019
comment
@ user35934 нужно понимать, что область видимости локальных переменных зависит от времени компиляции. В байт-коде есть только инструкции записи и чтения, адресующие индексы в кадре стека. Таким образом, могут быть висячие ссылки, но это также может работать и наоборот; при оптимизированном выполнении переменные могут все еще находиться «в области видимости», но объект, на который имеется ссылка, собирается, потому что впоследствии он не используется. -Xcomp просто отключает интерпретатор, а JIT-компилятор генерирует код, удаляющий ссылки сразу после последнего использования. Как уже говорилось, в реальных приложениях это обычно не имеет значения. - person Holger; 20.08.2019

get() метод для фантомных ссылок всегда возвращает null.

В тот момент, когда фантомная ссылка помещается в очередь, объект, на который она ссылается, уже собран сборщиком мусора. Вам необходимо хранить данные, необходимые для очистки, в отдельном объекте (например, вы можете создать подкласс PhantomReference).

Здесь вы можете найти пример кода и многое другое. подробное описание использования PhantomReferences.

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

person Alexey Ragozin    schedule 09.04.2017