【问题标题】:Injecting @Dependent CDI bean into EJB causes memory leak将@Dependent CDI bean 注入 EJB 会导致内存泄漏
【发布时间】:2020-04-28 21:38:20
【问题描述】:

使用 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 是无状态 EJB 时发生,但在 @ApplicationScoped 时不会发生。我认为 GlobalFactory 的 @ApplicationScoped 足以得到 OutOfMemoryError。

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

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

【问题讨论】:

    标签: jakarta-ee memory-leaks ejb cdi weld


    【解决方案1】:

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

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

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

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

    无状态会话 bean 必须属于 @Dependent 伪范围。

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

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

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

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

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

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

    【讨论】:

    • 从 CDI 我只需要注入和初始化,就像 Guice。无需调用 PreDestroy。书籍创建后保存在缓存中,不知道什么时候会从缓存中删除或停止在其他地方使用。
    • @Rustam 在你的情况下,我不会在书中保留对服务的任何引用。我会尝试在 bean 之外运行初始化逻辑,例如CDI 生产者方法。您的 GlobalFactory.createBookN() 函数是优秀的候选者。
    • 检查任何 Instance.get 创建 org.jboss.weld.contexts.SerializableContextualInstanceImpl。如果我从 GlobalSingleton 多次调用 GlobalService(当它 @Stateless 时)它会导致 OutOfMemoryError。虽然我有 1 个 GlobalSingleton 实例和 5 个 GlobalService 实例。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-12
    • 2013-06-30
    • 2014-08-26
    • 1970-01-01
    • 2012-10-17
    相关资源
    最近更新 更多