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

Mar*_*o13 6 java java-native-interface multithreading garbage-collection jvm

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

(我知道,这听起来很不寻常.所以这里有一些上下文,虽然它可能并不严格相关:该对象可能被认为是在JNI操作中使用的直接 对象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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


更新:正如评论指出:这是否可以工作在所有可能取决于精确的Executor实现(尽管我不得不更细致地分析).在给定的情况下,执行者将是一个ThreadPoolExecutor.

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

ThreadPoolExecutor有一个afterExecute方法.人们可以覆盖这种方法,然后使用反射的大锤潜入Runnable作为参数给出的实例.现在,可以简单地使用反射黑客走到这个引用,并用于runnable.getClass().getDeclaredFields()获取字段(即localObject字段),然后获取该字段的值.而且我认为应该让它观察到与原来的价值不同的价值.

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

现在,我强烈认为该字段可能无法删除.但是一些明确的参考(或至少更有说服力的论点)会很好.


更新2:根据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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

确保localObject在runnable中真正存在的只要它应该存在.但是我几乎不记得曾经被迫写过像这几行代码一样大声尖叫"粗暴黑客"的东西......

Hol*_*ger 4

如果JNI 代码获取直接缓冲区的地址,则JNI 代码本身应负责保存对直接缓冲区对象的引用,只要JNI 代码保存指针即可,例如使用NewGlobalRefand DeleteGlobalRef

\n

关于您的具体问题,直接在 JLS \xc2\xa712.6.1 中解决。实施最终确定

\n
\n

可以设计程序的优化转换,将可到达的对象数量减少到比天真地认为可到达的对象数量少。\xe2\x80\xa6

\n

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

\n
\n

(最后一句话很重要)

\n

该章中通过一个与您的例子并没有太大不同的例子来说明这一点。简而言之,实例localObject内的引用Runnable将使被引用对象的生命周期至少与实例的生命周期一样长Runnable

\n

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

\n

也就是说,方法内联是最简单的优化之一,之后 JVM 会检测到 aafterExecuteThreadPoolExecutor无操作。顺便说一句,Runnable传递给它的就是传递Runnable给 的execute,但是如果你使用该方法,它\xe2\x80\x99不会与传递给 相同submit,因为(仅)在后一种情况下,它\xe2\x80\ x99s包裹在RunnableFuture.

\n

请注意,即使该run()方法正在执行,也不会阻止实现\xe2\x80\x99s 实例的收集Runnable,如在Java 8\xe2\x80\x9d 中对强可达对象调用的 \xe2\x80\x9cfinalize()中所示。

\n

最重要的是,当你试图与垃圾收集器对抗时,你将如履薄冰。正如上面引用的第一句话所述: \xe2\x80\x9c可以设计优化程序的转换,以将可到达的对象数量减少到少于天真地认为可到达的对象数量。\xe2\x80\x9d 而我们可能会发现自己想得太天真\xe2\x80\xa6

\n

正如一开始所说,你可能会重新考虑责任。值得注意的是,当您的类有一个close()必须在所有线程完成其工作后调用来释放资源的方法时,此所需的显式操作已经足以防止资源的早期收集(假设该方法确实在正确的位置被调用)\xe2\x80\xa6

\n