You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vaadin 24.1.10多客户端未从Broadcaster解绑致内存泄漏求助

解决Vaadin 24.1.10中Broadcaster导致的内存泄漏问题

问题背景

使用Vaadin 24.1.10开发的应用通过Broadcaster实现视图部分更新,但频繁出现内存不足崩溃:内存占用从200MB飙升至4GB且无法释放。排查发现,打开多个注册广播视图的标签页后关闭整个浏览器时,仅部分客户端从Broadcaster解绑,剩余监听器持续留存;Broadcaster不断向已不存在的客户端发送数据,未被处理的数据大量缓存直至耗尽内存。尝试设置10秒心跳、在ui.access中检查ui.isAttached及UI.ifPresent等操作,均未解决问题。

核心问题分析

  1. WeakHashMap误用:当前用WeakHashMap存储监听器,但如果View实例被Vaadin UI上下文等强引用持有,WeakHashMap不会自动回收键,导致监听器残留。
  2. 解绑不彻底:浏览器强制关闭或崩溃时,View的onDetach事件可能未触发,监听器无法主动从Broadcaster移除。
  3. 无效任务缓存:即使客户端已断开,ui.access()仍会将任务加入队列,未执行的任务持续占用内存。

解决方案

一、可靠的客户端在线判断

  • 基于Push连接状态:启用Vaadin Push机制(通过@Push注解配置),利用WebSocket保持客户端与服务端的连接,UI.isConnected()方法可准确判断客户端是否在线(比isAttached更可靠,后者仅判断UI是否附加到会话,不验证连接状态)。
  • 会话销毁监听:注册SessionDestroyListener,当用户会话销毁时,批量移除该会话下所有UI对应的监听器。

二、Broadcaster改造(解决内存泄漏+避免无效数据缓存)

  1. 重构监听器存储结构:放弃WeakHashMap,改用ConcurrentHashMap<UI, Set<EventBusListener>>按UI分组管理监听器,便于批量清理无效UI的监听器。
  2. 定时自动清理:添加定时任务,定期检查并移除已断开连接的UI对应的监听器。
  3. 广播前过滤无效客户端:推送事件前先验证UI连接状态,仅向在线客户端发送数据,避免无效任务入队。

三、View层优化

  1. 绑定UI注册监听器:注册监听器时关联当前UI,确保解绑时可精准定位。
  2. 优化accessUiAndRun方法:用isConnected()替代isAttached()判断客户端状态,避免向已断开的客户端推送任务。

改造后代码示例

改造后的Broadcaster

import com.vaadin.flow.component.UI;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Broadcaster {
    // 按UI分组存储监听器,支持并发操作
    private static final ConcurrentHashMap<UI, Set<EventBusListener>> dashboardListeners = new ConcurrentHashMap<>();
    // 定时清理线程池,每30秒执行一次无效监听器清理(守护线程不阻止应用关闭)
    private static final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    });

    static {
        cleaner.scheduleAtFixedRate(Broadcaster::cleanInvalidListeners, 30, 30, TimeUnit.SECONDS);
    }

    public static void post(Object event) {
        // 仅向在线的UI推送事件
        dashboardListeners.forEach((ui, listeners) -> {
            if (ui.isConnected()) {
                ui.access(() -> listeners.forEach(listener -> listener.eventFired(event)));
            }
        });
    }

    public static void registerEventBusListener(UI ui, EventBusListener listener) {
        dashboardListeners.computeIfAbsent(ui, k -> ConcurrentHashMap.newKeySet()).add(listener);
        // UI销毁时自动移除对应监听器集合
        ui.addDetachListener(e -> dashboardListeners.remove(ui));
    }

    public static void unregisterEventBusListener(UI ui, EventBusListener listener) {
        dashboardListeners.computeIfPresent(ui, (k, listeners) -> {
            listeners.remove(listener);
            // 集合为空则移除键值对,避免空集合占用内存
            return listeners.isEmpty() ? null : listeners;
        });
    }

    private static void cleanInvalidListeners() {
        // 移除所有已断开连接的UI对应的监听器集合
        dashboardListeners.keySet().removeIf(ui -> !ui.isConnected());
    }
}

改造后的View

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.Component;

public class YourView implements EventBus.EventBusListener {

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        super.onAttach(attachEvent);
        UI ui = attachEvent.getUI();
        Broadcaster.registerEventBusListener(ui, this);
    }

    @Override
    protected void onDetach(DetachEvent detachEvent) {
        super.onDetach(detachEvent);
        UI ui = detachEvent.getUI();
        Broadcaster.unregisterEventBusListener(ui, this);
    }

    @Override
    public void eventFired(Object event) {
        accessUiAndRun(this, this::refreshStateAndFill);
    }

    public static void accessUiAndRun(Component component, Runnable runnable) {
        component.getUI().ifPresent(ui -> {
            // 用isConnected判断客户端是否在线,确保任务仅推送给活跃客户端
            if (ui.isConnected()) {
                ui.access(runnable);
            }
        });
    }

    private void refreshStateAndFill() {
        // 你的视图刷新逻辑
    }
}

补充配置

确保在应用主类或视图类上添加@Push注解启用Push机制:

import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.router.Route;

@Push
@Route("your-view-path")
public class YourView extends ... {
    // ...
}

内容的提问来源于stack exchange,提问作者ed_dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 23:02:10