首页 > 解决方案 > 将@Dependent CDI bean 注入 EJB 会导致内存泄漏

问题描述

使用 WildFly 18.0.1 创建多个 @Dependent 实例来测试内存泄漏

@Dependent
public class Book {
    @Inject
    protected GlobalService globalService;

    protected byte[] data;
    protected String id;

    public Book() {
    }

    public Book(GlobalService globalService) {
        this.globalService = globalService;
        init();
    }

    @PostConstruct
    public void init() {
        this.data = new byte[1024];
        Arrays.fill(data, (byte) 7);
        this.id = globalService.getId();
    }
}


@ApplicationScoped
public class GlobalFactory {
    @Inject
    protected GlobalService globalService;
    @Inject
    private Instance<Book> bookInstance;

    public Book createBook() {
        return bookInstance.get();
    }

    public Book createBook2() {
        Book b = bookInstance.get()
        bookInstance.destroy(b);
        return b;
    }

    public Book createBook3() {
        return new Book(globalService);
    }

}

@Singleton
@Startup
@ConcurrencyManagement(value = ConcurrencyManagementType.BEAN)
public class GlobalSingleton {

    protected static final int ADD_COUNT = 8192;
    protected static final AtomicLong counter = new AtomicLong(0);

    @Inject
    protected GlobalFactory books;

    @Schedule(second = "*/1", minute = "*", hour = "*", persistent = false)
    public void schedule() {
        for (int i = 0; i < ADD_COUNT; i++) {
            books.createBook();
        }
        counter.addAndGet(ADD_COUNT);
        System.out.println("Total created: " + counter);
    }

}

创建 200k 本书后,我得到 OutOfMemoryError。我很清楚,因为它写在这里

CDI | 应用程序/从属范围 | 内存泄漏 - javax.enterprise.inject.Instance<T> 未收集垃圾

CDI 应用程序和从属范围会合谋影响垃圾收集吗?

但我还有一个问题:

  1. 为什么只有在 Book 中的 GlobalService 是无状态 EJB 时才会发生 OutOfMemoryError,但在 @ApplicationScoped 时不会发生。我认为 GlobalFactory 的 @ApplicationScoped 足以得到 OutOfMemoryError。

  2. 哪种方法更好 createBook2() 或 createBook3()?两者都消除了 OutOfMemoryError 的问题

  3. createBook() 还有其他变体吗?

标签: jakarta-eememory-leaksejbcdiweld

解决方案


我对(1)印象深刻和惊讶。不得不尝试自己,确实如你所说!尝试了 WildFly 18.0.1 和 15.0.1,行为相同。我什至解雇了 jconsole,堆使用图有一个非常健康的锯状形状,对于这种@ApplicationScoped情况,内存在每次 GC 后准确地返回到基线。然后,我开始尝试。

我不敢相信 CDI 实际上是在销毁@Dependentbean 实例,所以我PreDestroyBook. 正如预期的那样,该方法从未被调用,但我开始获得 OOME,即使是@ApplicationScopedCDI bean!

为什么添加@PostConstruct一种使应用程序行为不同的方法?我认为正确的问题是相反的,即为什么要删除使@PostConstructOOME消失?由于 CDI 必须@Dependent使用其父对象销毁对象 - 在本例中为Instance<Book>.,因此它必须@DependentInstance. 调试一下,你会看到的。该列表是保留对所有已创建@Dependent对象的引用并最终导致内存泄漏的列表。显然(没有时间找到证据)Weld 正在应用优化:如果一个@Dependent对象在其依赖注入树中没有@PostConstruct方法,Weld 不会将其添加到此列表中。那就是(我的猜测)为什么(1)GlobalService@ApplicationScoped.

在将 EJB 注入 CDI bean 时,CDI 必须将其自己的生命周期与 EJB 生命周期绑定。显然(再次,我的猜测)CDI在EJB 绑定两个生命周期@PostConstruct时创建了一个钩子。GlobalService根据 JSR 365 (CDI 2.0) ch 18.2:

无状态会话 bean 必须属于@Dependent伪作用域。

因此,在其对象链中Book获取了一个钩子:@PostConstruct@Dependent

Book [@Dependent, no @PostConstruct] -> GlobalService [@Dependent, @PostConstruct]

因此,Instance<Book>需要引用Book它创建的每个,以便调用依赖EJB的@PostConstruct方法(由 CDI 隐式创建) 。GlobalService

解决了 (1) 的谜题(希望如此),让我们继续讨论 (2):

  • createBook2(): 缺点是用户必须知道目标 bean 是@Dependent. 如果有人改变了范围,那么销毁它是不合适的(除非你真的知道你在做什么)。然后保持对死实例的引用似乎令人毛骨悚然:)
  • createBook3():一个缺点是GlobalFactory必须知道Book. 也许这还不算太糟糕,对于书籍的工厂来说,了解它们的依赖关系是合理的。但是,您不会得到像@PostConstruct/之类的 CDI 好东西@PreDestroy,一本书的拦截器(例如,事务在 CDI 中被实现为拦截器)。另一个缺点是普通对象具有对 CDI bean 的引用。如果它们属于一个狭窄的范围(例如@RequestScoped),那么您可能会将对它们的引用保持在它们的正常寿命之外,从而产生不可预测的结果。

现在对于(3)以及最佳解决方案是什么,我认为这在很大程度上取决于您的确切用例。例如,如果您希望每个 都有完整的 CDI 设施(例如拦截器)Book,您可能希望跟踪您手动创建的书籍,并在适当时批量销毁。或者,如果 book 是一个 POJO,只需要设置它的 id,你就继续使用createBook3().


推荐阅读