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

问题描述 投票:2回答:1

使用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. 为什么只有在Book中的GlobalService是无状态EJB时才会发生OutOfMemoryError,而如果@ApplicationScoped则不会。我认为@ApplicationScoped for GlobalFactory足以获取OutOfMemoryError。

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

  3. 还有createBook()的其他变体吗?
jakarta-ee memory-leaks ejb cdi weld
1个回答
1
投票

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

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

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

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

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

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

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

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

已经解决了(1)的奥秘(希望),我们继续进行到(2):

  • createBook2():缺点是用户必须知道目标bean是@Dependent。如果有人更改了范围,则销毁它是不合适的(除非您真的知道自己在做什么)。然后保持对死实例的引用似乎令人毛骨悚然:)
  • createBook3():一个缺点是GlobalFactory必须知道Book的依赖性。也许还算不错,对于工厂来说,让书知道它们的依赖关系是合理的。但是,这样一来,您就不会得到像@PostConstruct / @PreDestroy这样的CDI好东西,即书籍的拦截器(例如,交易在CDI中被实现为拦截器)。另一个缺点是,普通对象具有对CDI bean的引用。如果它们属于较窄的范围(例如@RequestScoped),则您可能会超出对它们的正常使用期限,从而导致无法预测的结果。

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

© www.soinside.com 2019 - 2024. All rights reserved.