【问题标题】:RMI garbage collection of callbacks回调的 RMI 垃圾收集
【发布时间】:2022-01-24 15:55:20
【问题描述】:

我需要创建一个可以向客户端通知事件的 RMI 服务。 每个客户端在服务器上注册自己,客户端可以发出一个事件,服务器会将它广播给所有其他客户端。

程序可以运行,但是服务器上的客户端引用永远不会被垃圾回收,服务器用来检查客户端引用是否永远不会终止的线程。

所以每次客户端连接到服务器时,都会创建一个新线程并且永远不会终止。

Notifier 类可以注册和注销监听器。

广播方法调用每个注册的监听器并将消息发回。


public class Notifier extends UnicastRemoteObject implements INotifier{

    private List<IListener> listeners = Collections.synchronizedList(new ArrayList());
    public Notifier() throws RemoteException {
        super();
    }
    
    @Override
    public void register(IListener listener) throws RemoteException{
        listeners.add(listener);
    }
    
    @Override
    public void unregister(IListener listener) throws RemoteException{
        boolean remove = listeners.remove(listener);
        if(remove) {
            System.out.println(listener+" removed");
        } else {
            System.out.println(listener+" NOT removed");
        }
    }
    
    @Override
    public void broadcast(String msg) throws RemoteException {
        for (IListener listener : listeners) {
            try {
                listener.onMessage(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
}

监听器只是打印每条收到的消息。

public class ListenerImpl extends UnicastRemoteObject implements IListener {

    public ListenerImpl() throws RemoteException {
        super();
    }

    @Override
    public void onMessage(String msg) throws RemoteException{
        System.out.println("Received: "+msg);
    }

}

RunListener 客户端订阅一个侦听器,等待几秒钟以接收消息,然后终止。

public class RunListener {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry();
        INotifier notifier = (INotifier) registry.lookup("Notifier");
        ListenerImpl listener = new ListenerImpl();
        notifier.register(listener);
        Thread.sleep(6000);
        notifier.unregister(listener);
        UnicastRemoteObject.unexportObject(listener, true);

    }
}

RunNotifier 只是发布服务并定期发送消息。



public class RunNotifier {
    static AtomicInteger counter = new AtomicInteger();

    public static void main(String[] args) throws RemoteException, AlreadyBoundException, NotBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        INotifier notifier = new Notifier();
        registry.bind("Notifier", notifier);

        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    int n = counter.incrementAndGet();
                    System.out.println("Broadcasting "+n);
                    notifier.broadcast("Hello ("+n+ ")");
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        },5 , 5, TimeUnit.SECONDS);
        
        try {
            System.in.read();
        } catch (IOException e) {
        }
        executor.shutdown();
        registry.unbind("Notifier");
        UnicastRemoteObject.unexportObject(notifier, true);
    }

}

我看过很多关于 RMI 堆栈溢出的问答,但没有一个能解决这类问题。

我想我犯了一些非常大的错误,但我无法发现它。

如图所示,每个传入的连接都会创建一个新的 RMI RenewClean 线程,并且该线程永远不会终止。

一旦客户端断开连接并终止,RenewClean 线程将默默吞下所有抛出的 ConnectionException 并继续轮询一个永远不会回复的客户端。

附带说明一下,我什至尝试在 Notifier 类中只保留 IListener 的弱引用,但结果仍然相同。

【问题讨论】:

  • 有没有办法重新架构它,这样你就不会保留对客户端的对象引用?例如,为什么不使用队列来发送/接收消息?不要将对象排队,而是将消息排队。
  • System.out.println(listener+" removed"); 这一行是执行还是另一行?注意:您需要同步您对列表的遍历。
  • “已删除”。为简洁起见,我没有包括同步。
  • 即使不取消导出监听器也应该可以工作,事实上它甚至可以更好地工作,但它需要比 5 秒长得多。查看 RMI 系统属性页面中的默认 DGC 计时器和租用间隔。
  • @user207421 :UnicastRemoteObject.unexportObject(notifier, true); 仅用于客户端的“干净”退出。我运行它还在两个 JVM 上设置了 -Djava.rmi.dgc.leaseValue=1000,但无济于事。

标签: java rmi


【解决方案1】:

如果您卡在 JDK1.8 上,这可能不是很有帮助,但是当我在 JDK17 上测试时,为每个传入客户端 RMI RenewClean-[IPADDRESS:PORT] 创建的多个 rmi 服务器线程在服务器上被清理,并且不显示“永远不会您可能在 JDK1.8 上观察到的“终止”行为。这可能是 JDK1.8 的问题,或者只是您等待线程结束的时间不够长。

为了更快地清理,请尝试从默认值(3600000 = 1 小时)调整客户端线程垃圾收集设置的系统属性:

java -Dsun.rmi.dgc.client.gcInterval=3600000 ...

在我的服务器上,我在其中一个 API 回调中添加了这个:

Function<Thread,String> toString = t -> t.getName()+(t.isDaemon() ? " DAEMON" :"");

Set<Thread> threads = Thread.getAllStackTraces().keySet();
System.out.println("-".repeat(40)+" Threads x "+threads.size());
threads.stream().map(toString).forEach(System.out::println);

RMI 服务器启动后,它会打印线程名称并且没有“RMI RenewClean”实例:

---------------------------------------- Threads x 12

从客户端多次连接后,服务端报告了相应的“RMI RenewClean”实例:

---------------------------------------- Threads x 81

离开 RMI 服务器一段时间后,这些线程逐渐减少 - 不是 12 个线程 - 但足够低,表明 RMI 线程处理没有被许多不必要的守护线程填满:

---------------------------------------- Threads x 20

大约一个小时后,所有剩余的“RMI RenewClean”都被删除了 - 可能是由于在 VM 设置 sun.rmi.dgc.client.gcInterval=3600000 定义的间隔内执行的内务管理:

---------------------------------------- Threads x 13

另请注意,RMI 服务器在任何时候都会立即关闭 - “RMI RenewClean”守护线程不会阻止 rmi 服务器关闭。

【讨论】:

  • 我尝试将 gcInterval 减少到 1 分钟,并且它似乎有效,我将使用新的 JDK 再试一次。我想我将不得不接受这种延迟或实施不同的协议来交换数据。
  • @minus 在 JDK1.8 中,当这些线程比您预期的更长一点时,您观察到的不利影响是什么?
  • 我真的不知道我可以期待多少个连接,2 个被授予,但是一些 task 需要一个新的客户端连接,并且 tasks可以是任何东西(包括定期安排的作业),在这种情况下,我无法预测每小时将获得多少客户端连接。我担心有很多线程、套接字和内存被分配了。我认为即使不理想也是可以接受的。我希望自己做错了什么,并且可以干净、迅速地回收资源。
猜你喜欢
  • 1970-01-01
  • 2016-06-13
  • 2021-01-18
  • 2013-07-12
  • 1970-01-01
  • 2011-01-21
  • 1970-01-01
  • 2016-12-05
  • 1970-01-01
相关资源
最近更新 更多