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等操作,均未解决问题。
核心问题分析
- WeakHashMap误用:当前用
WeakHashMap存储监听器,但如果View实例被Vaadin UI上下文等强引用持有,WeakHashMap不会自动回收键,导致监听器残留。 - 解绑不彻底:浏览器强制关闭或崩溃时,View的
onDetach事件可能未触发,监听器无法主动从Broadcaster移除。 - 无效任务缓存:即使客户端已断开,
ui.access()仍会将任务加入队列,未执行的任务持续占用内存。
解决方案
一、可靠的客户端在线判断
- 基于Push连接状态:启用Vaadin Push机制(通过
@Push注解配置),利用WebSocket保持客户端与服务端的连接,UI.isConnected()方法可准确判断客户端是否在线(比isAttached更可靠,后者仅判断UI是否附加到会话,不验证连接状态)。 - 会话销毁监听:注册
SessionDestroyListener,当用户会话销毁时,批量移除该会话下所有UI对应的监听器。
二、Broadcaster改造(解决内存泄漏+避免无效数据缓存)
- 重构监听器存储结构:放弃
WeakHashMap,改用ConcurrentHashMap<UI, Set<EventBusListener>>按UI分组管理监听器,便于批量清理无效UI的监听器。 - 定时自动清理:添加定时任务,定期检查并移除已断开连接的UI对应的监听器。
- 广播前过滤无效客户端:推送事件前先验证UI连接状态,仅向在线客户端发送数据,避免无效任务入队。
三、View层优化
- 绑定UI注册监听器:注册监听器时关联当前UI,确保解绑时可精准定位。
- 优化
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
相关产品推荐
相关产品推荐

