删除未使用的字段是否会导致垃圾回收?

时间:2016-12-14 16:12:22

标签: java multithreading garbage-collection jvm java-native-interface

对于涉及异步操作的库,我必须保持对对象的引用,直到满足某个条件。

(我知道,这听起来很不寻常。所以这里有一些上下文,虽然它可能并不严格相关:该对象可能被认为是在JNI操作中使用的 direct ByteBuffer。 JNI操作将获取缓冲区的地址。此时,该地址只是一个“指针”,被视为对字节缓冲区的引用。该地址可以异步使用,稍后因此,在JNI操作完成之前,必须防止缓冲区被垃圾收集。) 功能

为实现这一目标,我实现了一个基本等同于此的方法:

private static void keepReference(final Object object)
{
    Runnable runnable = new Runnable()
    {
        @SuppressWarnings("unused")
        private Object localObject = object;

        public void run()
        {
            // Do something that does NOT involve the "localObject" ...
            waitUntilCertainCondition();

            // When this is done, the localObject may be garbage collected
        }
    };
    someExecutor.execute(runnable);
}

我们的想法是创建一个Runnable实例,该实例将所需对象作为字段,将此runnable抛出到执行程序中,让runnable等待直到满足条件。执行程序将保持对可运行实例的引用,直到它被完成为止。可运行的应该以保持对所需对象的引用。因此只有在满足条件后,执行程序才会释放runnable,因此本地对象将有资格进行垃圾回收。

localObject方法的正文中使用的run()字段 。可能编译器(或更准确地说:运行时)检测到这一点,并决定删除这个未使用的引用,从而允许对象过早地进行垃圾回收?

(我考虑过这方面的解决方法。例如,在像logger.log(FINEST, localObject);这样的“虚拟语句”中使用对象。但即便如此,也不能确定“智能”优化器不会做一些内联并仍然检测到对象没有真正使用过)

更新:正如评论中所指出的:这是否可以完全可能取决于确切的Executor实施(尽管我必须仔细分析一下)。在给定的情况下,执行者将是ThreadPoolExecutor

这可能是迈向答案的一步:

ThreadPoolExecutorafterExecute方法。一个可以覆盖此方法,然后使用反射大锤深入到作为参数给出的Runnable实例。现在,人们可以简单地使用反射黑客走到这个引用,并使用runnable.getClass().getDeclaredFields()来获取字段(即localObject字段),然后获取该字段的值。我认为它应该被允许观察那里的值与它原来的值不同。

另一条评论指出afterExecute的默认实现是空的,但我不确定这个事实是否会影响该字段是否可以删除的问题。

目前,我强烈假设该字段可能被删除。但是一些明确的参考(或至少更有说服力的论点)会很好。

更新2 :根据评论和answer by Holger,我认为不是删除“字段本身”可能是一个问题,但而是周围Runnable实例的GC。所以现在,我认为可以尝试这样的事情:

private static long dummyCounter = 0;
private static Executor executor = new ThreadPoolExecutor(...) {
    @Override
    public void afterExecute(Runnable r, Throwable t) {
        if (r != null) dummyCounter++;
        if (dummyCounter == Long.MAX_VALUE) {
            System.out.println("This will never happen", r);
        }
    }
}

确保runnable 中的localObject真正生存期限。但是我几乎不记得曾经被迫写过像这几行代码一样大声尖叫“粗暴黑客”的东西......

2 个答案:

答案 0 :(得分:4)

如果JNI代码获取直接缓冲区的地址,则只要JNI代码保存指针,例如JNI代码本身就应该保持对直接缓冲区对象的引用。使用NewGlobalRefDeleteGlobalRef

关于您的具体问题,请直接在JLS §12.6.1. Implementing Finalization

中解决
  

可以设计优化程序的转换,以减少可达到的对象数量,使其少于可以被认为可达的对象数量。 ...

     

如果对象字段中的值存储在寄存器中,则会出现另一个示例。 ...请注意,只有在引用位于堆栈上而不是存储在堆中时才允许这种优化。

(最后一句很重要)

在该章中通过一个与你的不太相似的例子来说明。简而言之,localObject实例中的Runnable引用将使引用对象的生命周期至少与Runnable实例的生命周期一样长。

那就是说,这里的关键点是Runnable实例的实际生命周期。由于上面指定的规则,如果它也被一个不受优化影响的对象引用,它将被认为是绝对有效的,即不受优化影响,但即使Executor也不一定是全局可见对象。

也就是说,方法内联是最简单的优化之一,之后JVM会检测到afterExecute的{​​{1}}是无操作。顺便说一句,传递给它的ThreadPoolExecutor 传递给Runnable的{​​{1}},但它与传递给Runnable的不一样},如果您使用该方法,在后一种情况下(仅),它是wrapped in a RunnableFuture

请注意,即使正在执行execute方法也不会阻止submit实现的实例的收集,如“finalize() called on strongly reachable object in Java 8”中所示。

最重要的是,当你试图打击垃圾收集器时,你会在薄冰上行走。正如上面引用的第一句话所述:“可以设计优化程序转换,以减少可达到的对象数量,使其少于可以被认为可达到的对象。”而我们所有人都可能发现自己的想法过于天真......

正如开头所说,你可以重新考虑责任。值得注意的是,当您的类具有run()方法时,必须调用该方法以在所有线程完成其工作后释放资源,此必需的显式操作已足以阻止资源的早期收集(假设确实在正确的位置调用了该方法)...

答案 1 :(得分:3)

在线程池中执行Runnable不足以防止对象被垃圾回收。 甚至"这个"可以收集!请参阅JDK-8055183

以下示例显示keepReference并未真正保留它。虽然vanilla JDK不会出现这个问题(因为编译器不够智能),但是当对ThreadPoolExecutor.afterExecute的调用被注释掉时,它可以被重现。这绝对是可能的优化,因为afterExecute在默认的ThreadPoolExecutor实现中是无操作的。

import java.lang.ref.WeakReference;
import java.util.concurrent.*;

public class StrangeGC {
    private static final ExecutorService someExecutor =
        Executors.newSingleThreadExecutor();

    private static void keepReference(final Object object) {
        Runnable runnable = new Runnable() {
            @SuppressWarnings("unused")
            private Object localObject = object;

            public void run() {
                WeakReference<?> ref = new WeakReference<>(object);
                if (ThreadLocalRandom.current().nextInt(1024) == 0) {
                    System.gc();
                }
                if (ref.get() == null) {
                    System.out.println("Object is garbage collected");
                    System.exit(0);
                }
            }
        };
        someExecutor.execute(runnable);
    }

    public static void main(String[] args) throws Exception {
        while (true) {
            keepReference(new Object());
        }
    }
}

你的黑客afterExecute将会起作用 你基本上发明了一种 Reachability Fence ,见JDK-8133348

你所面临的问题是众所周知的。它将作为JEP 193的一部分在Java 9中解决。将有一个标准API将对象明确标记为可访问:Reference.reachabilityFence(obj)

<强>更新

Javadoc commentsReference.reachabilityFence建议synchronized阻止作为替代构造以确保可达性。