对于涉及异步操作的库,我必须保持对对象的引用,直到满足某个条件。
(我知道,这听起来很不寻常。所以这里有一些上下文,虽然它可能并不严格相关:该对象可能被认为是在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
。
这可能是迈向答案的一步:
ThreadPoolExecutor
有afterExecute
方法。一个可以覆盖此方法,然后使用反射大锤深入到作为参数给出的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
真正生存期限。但是我几乎不记得曾经被迫写过像这几行代码一样大声尖叫“粗暴黑客”的东西......
答案 0 :(得分:4)
如果JNI代码获取直接缓冲区的地址,则只要JNI代码保存指针,例如JNI代码本身就应该保持对直接缓冲区对象的引用。使用NewGlobalRef
和DeleteGlobalRef
。
关于您的具体问题,请直接在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 comments至Reference.reachabilityFence
建议synchronized
阻止作为替代构造以确保可达性。