这是我提问之前的代码。首先有一个接口:
public interface SomeAction {
public void doAction();
}
然后有两类:
public class SomeSubscriber {
public static int Count;
public SomeSubscriber(SomePublisher publisher) {
publisher.subscribe(this);
}
public SomeAction getAction() {
final SomeSubscriber me = this;
class Action implements SomeAction {
@Override
public void doAction() {
me.doSomething();
}
}
return new Action();
}
// specify what to do just before it is garbage collected
@Override
protected void finalize() throws Throwable {
SomeSubscriber.Count++;
System.out.format("SomeSubscriber count: %s %n", someSubscriber.Count);
}
private void doSomething() {
// TODO: something
}
}
第二类:
public class SomePublisher {
private List<SomeAction> actions = new ArrayList<SomeAction>();
public void subscribe(SomeSubscriber subscriber) {
actions.add(subscriber.getAction());
}
}
这是用于测试两个类的代码:
public class Test {
//output: "the answer is: 0" for the 1st run after compilation and running attemptCleanUp() first, stays 0 upon repeat run
public static void main (String args []) {
System.out. println("am in main()");
SomePublisher publisher = new SomePublisher();
for (int i = 0; i < 10; i++) {
SomeSubscriber subscriber = new SomeSubscriber(publisher);
subscriber = null;
}
attemptCleanUp();
}
//output: "the answer is: 0" for the 1st run after compilation and running attemptCleanUp() first, rising to 10, 20, 30 ...upon repeat run
public static void answerIsNot0() {
System.out. println("am in answerIsNot0()");
SomePublisher publisher = new SomePublisher();
for (int i = 0; i < 10; i++) {
SomeSubscriber subscriber = new SomeSubscriber(publisher);
subscriber = null;
}
attemptCleanUp();
}
private static void attemptCleanUp() {
threadMessage("Before the gc attempt, the answer is: " + SomeSubscriber.Count);
System.gc();
System.runFinalization();
threadMessage("After the gc attempt, the answer is: " + SomeSubscriber.Count);
}
private static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
}
main()的打印输出显示SomeSubscriber.Count值为1到10,而最后一行生成The answer is: 0
,如下所示:
am in main()
main: Before the gc attempt, the answer is: 0
SomeSubscriber count: 1
SomeSubscriber count: 2
SomeSubscriber count: 3
SomeSubscriber count: 4
SomeSubscriber count: 5
SomeSubscriber count: 6
SomeSubscriber count: 7
SomeSubscriber count: 8
SomeSubscriber count: 9
SomeSubscriber count: 10
main: After the gc attempt, the answer is: 0
而对于answerIsNot0(),The answer is: <num>
内的数字总是与SomeSubscriber count:
序列中的最后一个数字匹配。
我的问题是:首先,非零值是否表明垃圾收集确实发生了10次?这与以下观点相矛盾,即10个subscriber
仍然由publisher
实例中的本地类Action
的实例引用,因此不进行垃圾收集。其次,SomeSubscriber.Count的值在main(String-args[]){}方法的最后一条语句处发生了什么变化,而在answerIsNot0()方法处没有发生变化?换句话说,为什么同一个代码放在main()中与放在answerIsNot0()中时对SomeSubscriber.Count
产生不同的效果?
首先,垃圾收集和终结之间存在显著差异。两者都可能具有与实现相关的行为,这是故意未指定的,但至少可以保证,在抛出OutOfMemoryError
之前,虚拟机将执行垃圾收集以尝试回收内存。
另一方面,终结器根本不能保证运行。从技术上讲,终结只能在垃圾收集器确定对象不可访问并将其排入队列之后运行。
这意味着finalize()
方法不适合告诉您在正常情况下对象是否会被垃圾收集,即如果类没有自定义的finalize()
方法。
尽管如此,你的测试似乎击中了要害,这引发了可达性的问题:
JLS,§12.6.1。实施定稿…可达对象是可以在任何潜在的持续计算中从任何活动线程访问的任何对象。
很明显,如果没有变量包含对对象的引用,则没有"潜在的连续计算"可以访问它。这是检查这一点的最简单方法。尽管如此,在您的示例中,没有任何潜在的连续计算可以访问publisher
对象,因为没有代码执行对变量的任何访问。这更难检测,因此在JVM优化代码之前不会发生这种情况。§12.6.1明确规定:
程序的优化转换可以设计为将可访问的对象数量减少到少于那些天真地认为可访问的。例如,Java编译器或代码生成器可以选择将不再使用的变量或参数设置为null,以使此类对象的存储可以更快地回收。
另请参阅"当对象仍在作用域中时,java是否可以完成它?">
这似乎是你的问题。在一个没有得到最大限度优化的短时间运行的程序中,局部变量引用的一些未使用的对象可能不会立即被回收,而当它在多次运行后得到更深入的优化时,它们可能会更早地用相同的代码被回收。无论是main
方法还是另一个方法都不那么重要,重要的只是它被调用的频率或运行的时间(被视为热点),或者更准确地说,它在JVM的生命周期中会在多大程度上得到优化。
代码的另一个问题与以下内容有关:
JLS,第12.6节。类实例的定稿Java编程语言不指定哪个线程将调用任何给定对象的终结器。
需要注意的是,许多终结器线程可能是活动的(这有时在大型共享内存多处理器上是需要的),并且如果连接的大型数据结构成为垃圾,则可以同时调用该数据结构中每个对象的所有终结方法,每个终结器调用都在不同的线程中运行
Java编程语言对finalize方法调用不强制执行排序。终结器可以按任何顺序调用,甚至可以同时调用。
例如,如果一组未最终确定的循环链接对象变得不可访问(或终结器可访问),那么所有对象可能会一起变得可最终确定。最终,这些对象的终结器可以按任何顺序调用,甚至可以使用多个线程并发调用。如果自动存储管理器后来发现对象不可访问,则可以回收它们的存储
由于您没有采取任何措施来确保对变量SomeSubscriber.Count
的线程安全访问,因此可能会出现很多不一致。即使在终结器线程中发生了更改,也能从主线程中看到零,这只是其中之一。幸运的是,您看到了从1到10的递增数字,显然JRE中只有一个终结器线程。由于缺乏线程安全性,您可能会看到任意顺序的数字,但也有一些数字会多次出现,而另一些则会丢失,根本不一定是在十对象结束后达到十。
本地类Action
保留了对SomeSubscriber
的引用(因此您可以在其上调用doSomething()
。Action
的实例可以通过SomePublisher
访问。因此SomeSubscriber
的实例在main
方法结束时仍然可以访问。
这使得他们没有资格进行垃圾收集。所以他们没有被收集。
对于两个不同的结果,我假设您相继运行了两个方法。您得到的answer 10
是从要运行的第一个版本中收集的10个实例。(方法一结束,SomePublisher
就超出了范围,可以收集,所有参考文献都指向Action
和SomeSubscriber
)
此外,System.gc();
只是垃圾收集应该运行的一个提示,不能保证在方法运行后收集所有内容。