为什么我应该将 Weak/SoftReference 与 ReferenceQueue 参数一起使用?为什么我不能轮询原始引用并检查是否为空?
Why should I use Weak/SoftReference with ReferenceQueue argument?Why cannot I poll original reference and check if null?
据我所知,检测对象何时被收集并且内存已经空闲以供 weak/soft 引用的工作用例会轮询此队列,当引用出现在队列中时,我们可以确保内存空闲。
WeakReference ref = new WeakReference (new Object())
为什么我不能轮询 ref
并检查它是否变为空?
P.S.
根据评论中提供的link:
If the garbage collector discovers an object that is weakly reachable,
the following occurs:
1.The WeakReference object's referent field is set
to null, thereby making it not refer to the heap object any longer.
2.The heap object that had been referenced by the WeakReference is
declared finalizable.
3.When the heap object's finalize() method is run
and its memory freed, the WeakReference object is added to its
ReferenceQueue, if it exists.
因此,如果这篇文章写的是真实的,并且这些步骤有序的弱引用在步骤之后变为空,但对象仅在第 3 步添加到队列中。
这是真的吗?
这是为什么?
让我们研究代码:
工作规范示例:
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
// This will block till it is GCd
Reference prefFromQueue;
while (true) {
prefFromQueue = queue.remove();
if (prefFromQueue != null) {
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
此代码输出:
Awaiting for GC
Invoking GC
Referenced GC'd
null
好的,我明白它为什么起作用了。
让我们稍微更改一下代码:
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
while (true) {
if (pRef.get() == null) {
Thread.sleep(100);
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
此变体挂在 while 循环中并输出:
Awaiting for GC
Invoking GC
请解释此行为。
只要原始引用可用于程序的任何部分,就不会被垃圾回收。访问原始引用以检查它是否为 null 将阻止对象被释放。这就是为什么您需要特殊的引用类型来处理这种情况。
只需稍加修改,您的代码就会产生预期的结果。请参阅下面的代码片段。
在您的代码中,行
pRef.get() == null
会将 pRef.get()
分配给方法 run()
框架中的临时槽。条件计算后的事件,不会自动清除slot。
垃圾收集器将堆栈上活动帧中的所有 slots/local 变量视为 GC 根。您无意中创建了对对象的强引用,因此它不会被清除。
我修改了版本,我已将 pRef.get()
移动到嵌套方法。一旦从方法执行returns,它的框架就被处理掉,所以对对象的引用仍然存在以防止 GC 收集它。
当然,如果 JVM 重新编译 run()
方法并内联 isRefernceCollected(pRef)
调用,它可能会再次中断。
总而言之,引用队列为您提供了确定且高效的方式来处理引用。池可以工作,但它很脆弱,并且取决于 javac 和 JVM JIT 的代码编译。
修改后的代码片段。
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
while (true) {
if (isRefernceCollected(pRef)) {
Thread.sleep(100);
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
protected boolean isRefernceCollected(final WeakReference pRef) {
return pRef.get() == null;
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
输出
Awaiting for GC
Invoking GC
Referenced GC'd
null
如 所述,临时引用将保留在堆栈帧的槽中是不正确的。但是,当您在紧密循环中轮询变量时,优化器可能会创建代码来保留多次迭代的引用,而不是每次都从主内存中读取值。由于 System.gc()
只是一个提示,而且您只调用了一次,因此无法保证此特定的 gc 循环会发现此对象无法访问(如果该循环曾经发生过)。在那之后,由于没有后续的 GC 周期,您的轮询循环将永远 运行。
您还错放了 sleep
电话。它会在 检测到集合后 休眠,这是没有意义的。如果你改变
while (true) {
if (pRef.get() == null) {
Thread.sleep(100);
break;
}
}
至
while (true) {
if (pRef.get() == null) {
break;
}
Thread.sleep(100);
}
它适用于大多数环境。但是你不需要这么复杂的例子:
WeakReference<Object> ref=new WeakReference<>(new Object());
int count=0;
while(ref.get()!=null) {
System.gc();
count++;
}
System.out.println("collected after "+count+" polls");
适用于大多数系统。在我的系统上,它会在第一个周期收集对象。
那么轮询 ReferenceQueue
和轮询 Reference
之间的主要区别是什么?
顾名思义,ReferenceQueue
是一个队列,允许处理多个引用。您可以轮询 ReferenceQueue
以获取第一个收集对象(如果有的话),而不是一个接一个地轮询数千个引用以查明其中是否有一个收集的指示对象。如果收集了 none 个,那么您在第一次投票后就完成了。
因此,参考队列的轮询成本与收集到的对象数量成比例,而不是与现有对象总数成比例。一个典型的例子是WeakHashMap
。收集一个键后,必须从后备数组中删除仍然引用该值的关联条目。无需遍历整个数组,检查每个条目是否存在键,只需轮询引用队列以获取收集的键。由于扩展引用对象已经记住了哈希码,所以可以在O(1)
时间内在数组中找到。这是至关重要的,因为没有任何人必须调用的专用清理方法。由于它像普通地图一样工作,只是具有弱键语义,因此轮询透明地发生在该地图上的每个方法调用上。所以一定要快
据我所知,检测对象何时被收集并且内存已经空闲以供 weak/soft 引用的工作用例会轮询此队列,当引用出现在队列中时,我们可以确保内存空闲。
WeakReference ref = new WeakReference (new Object())
为什么我不能轮询 ref
并检查它是否变为空?
P.S.
根据评论中提供的link:
If the garbage collector discovers an object that is weakly reachable, the following occurs: 1.The WeakReference object's referent field is set to null, thereby making it not refer to the heap object any longer.
2.The heap object that had been referenced by the WeakReference is declared finalizable. 3.When the heap object's finalize() method is run and its memory freed, the WeakReference object is added to its ReferenceQueue, if it exists.
因此,如果这篇文章写的是真实的,并且这些步骤有序的弱引用在步骤之后变为空,但对象仅在第 3 步添加到队列中。
这是真的吗?
这是为什么?
让我们研究代码:
工作规范示例:
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
// This will block till it is GCd
Reference prefFromQueue;
while (true) {
prefFromQueue = queue.remove();
if (prefFromQueue != null) {
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
此代码输出:
Awaiting for GC
Invoking GC
Referenced GC'd
null
好的,我明白它为什么起作用了。
让我们稍微更改一下代码:
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
while (true) {
if (pRef.get() == null) {
Thread.sleep(100);
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
此变体挂在 while 循环中并输出:
Awaiting for GC
Invoking GC
请解释此行为。
只要原始引用可用于程序的任何部分,就不会被垃圾回收。访问原始引用以检查它是否为 null 将阻止对象被释放。这就是为什么您需要特殊的引用类型来处理这种情况。
只需稍加修改,您的代码就会产生预期的结果。请参阅下面的代码片段。
在您的代码中,行
pRef.get() == null
会将 pRef.get()
分配给方法 run()
框架中的临时槽。条件计算后的事件,不会自动清除slot。
垃圾收集器将堆栈上活动帧中的所有 slots/local 变量视为 GC 根。您无意中创建了对对象的强引用,因此它不会被清除。
我修改了版本,我已将 pRef.get()
移动到嵌套方法。一旦从方法执行returns,它的框架就被处理掉,所以对对象的引用仍然存在以防止 GC 收集它。
当然,如果 JVM 重新编译 run()
方法并内联 isRefernceCollected(pRef)
调用,它可能会再次中断。
总而言之,引用队列为您提供了确定且高效的方式来处理引用。池可以工作,但它很脆弱,并且取决于 javac 和 JVM JIT 的代码编译。
修改后的代码片段。
public class TestPhantomRefQueue {
public static void main(String[] args)
throws InterruptedException {
Object obj = new Object();
final ReferenceQueue queue = new ReferenceQueue();
final WeakReference pRef =
new WeakReference(obj, queue);
obj = null;
new Thread(new Runnable() {
public void run() {
try {
System.out.println("Awaiting for GC");
while (true) {
if (isRefernceCollected(pRef)) {
Thread.sleep(100);
break;
}
}
System.out.println("Referenced GC'd");
System.out.println(pRef.get());
} catch (Exception e) {
e.printStackTrace();
}
}
protected boolean isRefernceCollected(final WeakReference pRef) {
return pRef.get() == null;
}
}).start();
// Wait for 2nd thread to start
Thread.sleep(2000);
System.out.println("Invoking GC");
System.gc();
}
}
输出
Awaiting for GC
Invoking GC
Referenced GC'd
null
如 System.gc()
只是一个提示,而且您只调用了一次,因此无法保证此特定的 gc 循环会发现此对象无法访问(如果该循环曾经发生过)。在那之后,由于没有后续的 GC 周期,您的轮询循环将永远 运行。
您还错放了 sleep
电话。它会在 检测到集合后 休眠,这是没有意义的。如果你改变
while (true) {
if (pRef.get() == null) {
Thread.sleep(100);
break;
}
}
至
while (true) {
if (pRef.get() == null) {
break;
}
Thread.sleep(100);
}
它适用于大多数环境。但是你不需要这么复杂的例子:
WeakReference<Object> ref=new WeakReference<>(new Object());
int count=0;
while(ref.get()!=null) {
System.gc();
count++;
}
System.out.println("collected after "+count+" polls");
适用于大多数系统。在我的系统上,它会在第一个周期收集对象。
那么轮询 ReferenceQueue
和轮询 Reference
之间的主要区别是什么?
顾名思义,ReferenceQueue
是一个队列,允许处理多个引用。您可以轮询 ReferenceQueue
以获取第一个收集对象(如果有的话),而不是一个接一个地轮询数千个引用以查明其中是否有一个收集的指示对象。如果收集了 none 个,那么您在第一次投票后就完成了。
因此,参考队列的轮询成本与收集到的对象数量成比例,而不是与现有对象总数成比例。一个典型的例子是WeakHashMap
。收集一个键后,必须从后备数组中删除仍然引用该值的关联条目。无需遍历整个数组,检查每个条目是否存在键,只需轮询引用队列以获取收集的键。由于扩展引用对象已经记住了哈希码,所以可以在O(1)
时间内在数组中找到。这是至关重要的,因为没有任何人必须调用的专用清理方法。由于它像普通地图一样工作,只是具有弱键语义,因此轮询透明地发生在该地图上的每个方法调用上。所以一定要快