将@Dependent CDI bean注入EJB导致内存泄漏

Injecting @Dependent CDI bean into EJB causes memory leak

使用 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 | Application / Dependent Scope | Memory Leak - javax.enterprise.inject.Instance<T> Not Garbage Collected

CDI Application and Dependent scopes can conspire to impact garbage collection?

但我还有一个问题:

  1. 为什么OutOfMemoryError只发生在Book中的GlobalService是stateless EJB,而不是@ApplicationScoped。我认为 GlobalFactory 的 @ApplicationScoped 足以导致 OutOfMemoryError。

  2. createBook2() 或 createBook3() 哪种方法更好?两者都解决了 OutOfMemoryError

  3. 的问题
  4. createBook() 有其他变体吗?

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

我无法相信 CDI 实际上正在破坏 @Dependent bean 实例,所以我向 Book 添加了一个 PreDestroy 方法。 该方法从未像预期的那样被调用,但我开始获得 OOME,即使是 @ApplicationScoped CDI bean!

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

时 (1) 起作用

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

A stateless session bean must belong to the @Dependent pseudo-scope.

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

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

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

解开了 (1) 的谜团(希望)让我们继续 (2):

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

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