main() 方法在 java 中是否以不同方式处理垃圾 collection

Does main() method handle garbage collection differently in java

这是我提问前的代码。首先有一个接口:

public interface SomeAction {
    public void doAction();
}

然后有两个classes:

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
    }
}

第二个class:

public class SomePublisher {
    private List<SomeAction> actions = new ArrayList<SomeAction>();

    public void subscribe(SomeSubscriber subscriber) {
        actions.add(subscriber.getAction());
    }
}

这是用来测试两个classes的代码:

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: 系列中的最后一个数字匹配。

我的问题是:首先,non-zero 值是否表明垃圾 collection 确实发生了 10 次?这与 publisher 实例中的 10 个 subscriber 仍然被本地 class Action 实例引用的概念相矛盾,因此不受 garbage-collection。其次,SomeSubscriber.Count 的值如何在 main (String args []) {} 方法的最后语句中发生变化,而不是在 answerIsNot0() 方法中发生变化?换句话说,为什么相同的代码放在 main() 中而不是放在 answerIsNot0() 中时对 SomeSubscriber.Count 产生不同的效果?

本地 class Action 保留对 SomeSubscriber 的引用(因此您可以在其上调用 doSomething()Action 的实例可以通过SomePublisher。因此 SomeSubscriber 的实例在 main 方法结束时仍然可以访问。

这使得它们不符合垃圾回收的条件。所以没有收集到。

对于这两种不同的结果,我假设你 运行 这两种方法一个接一个。你得到的answer 10是从第一个版本到运行收集的10个实例。 (一旦方法结束 SomePublisher 超出范围并且可以被收集,所有对 ActionSomeSubscriber 的引用)

另外System.gc();只是提示垃圾收集应该运行,没有保证运行在方法运行之后收集所有东西。

首先,垃圾收集终结之间存在显着差异。两者都可能具有依赖于实现的行为,这是有意未指定的,但至少可以保证在抛出 OutOfMemoryError 之前,虚拟机将执行垃圾收集以尝试回收内存。

另一方面,终结器根本不能保证 运行。从技术上讲,只有在 运行 垃圾收集器确定对象无法访问并将它们排入队列后 运行 才能完成。

这意味着 finalize() 方法不适合告诉您对象是否会在正常情况下被垃圾收集,即如果 class 没有自定义 finalize() 方法.

不过,您的测试似乎遇到了问题,这引发了可达性的问题:

JLS, §12.6.1. Implementing Finalization

… A reachable object is any object that can be accessed in any potential continuing computation from any live thread.

很明显,如果没有变量持有对对象的引用,则任何“潜在的持续计算”都无法访问它。这是最简单的检查方法。尽管如此,在您的示例中,没有潜在的连续计算可以访问 publisher 对象,因为没有代码执行对该变量的任何访问。这很难检测到,因此在代码被 JVM 优化之前不会发生。 §12.6.1 明确指出:

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.

另见“Can java finalize an object when it is still in scope?

这似乎是你的问题。在未得到最大限度优化的 short-running 程序中,局部变量引用的一些未使用的对象可能不会立即被回收,而当它在多个 运行秒。它是 main 方法还是其他方法并不重要,重要的是它被调用的频率或持续时间 运行s(被认为是热点),或者更准确地说,在 JVM 的生命周期中它将优化到什么程度。

您的代码的另一个问题与以下内容有关:

JLS, §12.6. Finalization of Class Instances

The Java programming language does not specify which thread will invoke the finalizer for any given object.

It is important to note that many finalizer threads may be active (this is sometimes needed on large shared memory multiprocessors), and that if a large connected data structure becomes garbage, all of the finalize methods for every object in that data structure could be invoked at the same time, each finalizer invocation running in a different thread.

The Java programming language imposes no ordering on finalize method calls. Finalizers may be called in any order, or even concurrently.

As an example, if a circularly linked group of unfinalized objects becomes unreachable (or finalizer-reachable), then all the objects may become finalizable together. Eventually, the finalizers for these objects may be invoked, in any order, or even concurrently using multiple threads. If the automatic storage manager later finds that the objects are unreachable, then their storage can be reclaimed.

由于您没有采取任何措施来确保对变量 SomeSubscriber.Count 的线程安全访问,因此可能会出现很多不一致之处。即使在终结器线程中更改了主线程,但从主线程看到零只是其中之一。您已经很幸运地看到了从 1 到 10 的递增数字,显然您的 JRE 中只有一个终结器线程。由于缺乏线程安全性,您可能会看到任意顺序的数字,但也有一些数字出现多次而另一些丢失,根本不一定在十个对象完成后到达十个。