冷流和出版商,垃圾收集器是否充分意识到它真正的死亡?

Cold streams and publishers, is the Garbage Collector fully aware of its true death?

    final Consumer<T> localConsumer = this::accept;
    insatnceSubscriptorManager.baseSubscribe(
            isActive -> {
                if (isActive) publisher.add(localConsumer);
                else publisher.remove(localConsumer);
            }
    );

instanceSubscriptorManager 无法再接收任何布尔值时,要执行的最后一件事将是 false 作为 isActive,从 publisher 中删除 localConsumer

localConsumer 如何“意识到”instanceSubscriptorManager 将无法在将来的某个时候(再次)收到 isActive -> true,特别是因为 class 是自-引用自身?

简而言之,此 class 由两端持有,this::acceptpublisher 持有,而 publisher 又由 this.insatnceSubscriptorManager 持有,接收真或假。

这告诉我,JVM 必须创建一个引用索引,而不是从 OOP 的角度来看事物,而是将所有变量,本地变量或实例化变量视为独立节点。

如果传递 instanceSubscriptorManager true 或 false 的“东西”被 GC,垃圾收集器是否有足够的理由确定不应再捕获 baseSubscribe 接口捕获的引用,释放过程中的发布者?

或者我们还需要手动执行 unsubscribe() 吗?

我想知道收集的分支的“深度”是否有限制,有多少 subscribers/Consumers 可以相互链接,直到 GC 不能再将它们全部分离,如果一个节点被分离?

没有,像你写的那样? publisher 坚持不懈,永远不会被 GC。

廉价的修复方法是 'just call unsubscribe in a timely fashion',但这对您来说可能就像告诉截肢者只 'grow the leg back'.

一样有用

可以拥有一个系统,该系统将根据对象的 GC 资格在适当的时间'auto unsubscribe' 事件侦听器。

为什么publisher不能被GC?

如果您已经清楚这一点并且只是需要确认,请滚动到下一部分。

publisher 变量被克隆。这就是为什么您在 lambda 中使用的任何变量都需要声明为 final 或 'effectively final'(意思是:如果您将 final 推到它上面,那不会有问题,其中编译器会为你做的)。只是复制发生变化的东西会非常混乱。

因此,出于 GC 的目的,此处的 lambda(从 isActive -> 到大括号末尾的所有内容)被封装在一个对象中,并且对 publisher 指向的任何内容的引用是这个对象的一部分。比如,它有一个字段,具有该值。

然后将此对象传递给 baseSubscribe 方法。

假设:

  • instanceSubscriptorManager 指向不符合 GC 条件的对象。
  • 您可以 'get' 从该对象(通过其字段,代码是否存在于 运行 时间的任何地方实际执行此操作无关紧要)到处理程序,然后从那里到 publisher.

然后,瞧 - 无论发布者指向什么,GCing 现在都不会发生。它是 'stuck',因为可以从 instanceSubscriptorManager 永久访问,我们刚刚公理地说不会消失。 (但是,如果 publisher 是 only 获得 instanceSubscriptorManager 的方式,而 instanceSubscriptorManageronly 获得的方式到 publisher,这里没有问题 - java 不会因为无法清理循环孤儿链而受到影响,这与苹果针对 ObjC 的 refcount GC impl 不同,我认为 swift ).

智能退订听起来很棒。那是什么?

这(短暂事件处理程序的管理)是一个棘手的难题,几乎每个事件系统都在 'solves' 通过为您提供 subscribe 方法,unsubscribe 方法,以及对吉祥如意的真诚祝愿。您弄清楚如何使用这 2 个原语来管理它,这就是您所得到的。

其中,对于许多想要使用短暂处理程序的应用程序域('fleeting' 如:您希望它们符合 GC 的条件,并且如果应用 运行s forever) - 糟透了.

unsubscribe 部分感觉如此粗糙的一个明显原因是大多数应用程序域根本没有短暂的处理程序。在应用程序的生命周期内调用的次数 .subscribe() 是一个常量值,您传递给事件源的处理程序对象 打算 永远存在。

但是当不是你想要的东西时,正如你运行到这里(即,你想要短暂的处理程序),使用起来要方便得多java.lang.ref 包中的工具:WeakReferences 和引用队列。

看,可以订阅java自带的GC系统!您可以将一个对象变成软引用或弱引用(区别?java文档会解释,它会变得有点复杂。您可能需要弱引用),然后询问 GC 系统它是否指向该对象at 是否被 GC,甚至要求在被 GC 时得到通知。

这为 subscribe/unsubscribe 带来了不同的范例。而不是:

  1. 订阅以接收事件通知(并顺便将该对象锁定为不可进行 GC,因为它现在可由事件源访问),并且
  2. 当您不再需要它时,调用 unsubscribe 这也将放弃从事件源到对象的可达性;它将不再阻碍 GC

你可以做得更好。简单地做:

  1. 订阅接收事件通知,同时传递 'watch'。事件源持有对 watch(见下文)对象的弱引用,一旦 watch 对象被 GC 处理,事件源将自动取消订阅事件处理程序。因为它通过 weakref 间接了解手表,所以事件源不会妨碍手表的 GC 资格。这取决于想要订阅事件的代码,以确保在您想要停止接收通知之前手表不会被 GC。当你停止接收事件时,这个系统也是模糊的:Java 不会 立即 将无法访问的对象标记为符合 GC 条件,也不会触发引用队列以通知任何保持即时跟踪手表的 GC 能力。它会解决它的。
  2. ...没有第 2 步。退订只是在您需要的时候发生。

特别是,您有 java.lang.ref.WeakReference 的概念,它允许您应用一个间接层并断开引用链:

class SomethingLongLived {
   private final Object field;

   void code() {
     field = new Object();
   }
}

在上面,我们创建的那个对象在 SomethingLongLived 的实例消失之前不能被 GC,就这些例子而言,它不会。但现在可以是:

class SomethingLongLived {
   private final WeakReference<Object> field;

   void code() {
     field = new WeakReference<Object>(new Object());
   }
}

WeakReference 对象本身 当然不能 被收集,但一般的想法是,WeakRef 对象本身很小,并且可能您感兴趣的对象很大,或者您对它感兴趣只是为了跟踪它是否有资格获得 GC。您可以向 WeakReference 对象请求 'referrant',但如果引用对象已经被 GC 处理或处于最后阶段(对象之前发生的最后一件事之一),那将 return null它占用的内存真正被标记为 'free',是任何指向它的 WeakReference 链接都被取消了。之后,没有任何方法可以访问该对象,期间)。

您的事件触发代码可以像首先检查关联的 weakref 一样简单。如果为 null,则 NOT 将事件发送到关联的事件处理程序,并从处理程序列表中删除 weakref+eventhandler 对。

请注意,它仍然可能很复杂:如果事件处理程序本身有办法到达监视对象,那么 它们永远不会消失:事件源可以到达处理程序,处理程序可以到达手表:没有任何东西会被 GC。因此手表管理有时有点棘手。

如果需要,您可以通过使事件处理程序本身成为监视来简化此系统 - 事件源不会阻止订阅它的任何处理程序的 GC;那是来电者的责任。但在实践中,这并不能很好地解决问题,并且您将面临创建一个阻止所有 GC 的链的巨大风险。拥有琐碎的 watch 对象(字面意思是琐碎的:new Object() - 它的唯一目的是让事件源观察它是否被 GCed)并找到一个地方来存储对这个东西的引用要好得多,这样 'if that place gets GCed - the watch goes with it, and the event handler is unsubscribed automatically as well'.

就其价值而言,我不知道有任何事件源系统以这种方式工作,这有点奇怪,因为看起来 unsubscribe 好得多 ] 方法。

基本实现:

public class WindowSystem {
  // This class represents an 'event source'.
  // you can subscribe to these.

  public interface MouseMovedHandler {
    void mouseAt(int x, int y);
  }

  @Value
  private static class HandlerContainer {
    WeakReference<Object> watch;
    MouseMovedHandler handler;
  }

  private final List<HandlerContainer> handlers =
    new ArrayList<HandlerContainer>();

  // NB: WARNING - this code is not threadsafe.
  // it needs to be - exercise left to the reader.
  public void subscribe(@NonNull Object watch, @NonNull MouseMovedHandler handler) {
    handlers.add(new HandlerContainer(new WeakReference<>(watch), handler));
  }

  void mainLoop() {
    // This code is called a lot by some separate thread managing
    // the window

    // ... the mouse is moved...
    fireMouseMoveEvent(x, y);
  }

  private void fireMouseMoveEvent(int x, int y) {
    var it = handlers.iterator();
    while (it.hasNext()) {
      var c = it.next();
      if (c.getWatch().get() == null) {
        it.remove();
      } else {
        c.getHandler().mouseAt(x, y);
      }
    }
  }
}

还有很多:从巨大的数组列表中间清除内容效率不高,有时您需要单独的线程来触发事件,显然上面的内容会爆炸,因为您正在修改来自2 个单独的线程(window 循环,以及调用 .subscribe 的任何代码),因此它需要使用来自 java.util.concurrent 的相关列表类型(也检查相关的 WeakHashMap这类事情),或者添加一些基于 synchronizedj.u.c.Lock 的代码,等等

但是,希望这个想法是明确的:这就是你 'auto unsubscribe' 基于 GC 系统的事情。