jakarta-ee - 将@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> 未收集垃圾
但我还有一个问题:
为什么只有在 Book 中的 GlobalService 是无状态 EJB 时才会发生 OutOfMemoryError,但在 @ApplicationScoped 时不会发生。我认为 GlobalFactory 的 @ApplicationScoped 足以得到 OutOfMemoryError。
哪种方法更好 createBook2() 或 createBook3()?两者都消除了 OutOfMemoryError 的问题
- createBook() 还有其他变体吗?
解决方案
我对(1)印象深刻和惊讶。不得不尝试自己,确实如你所说!尝试了 WildFly 18.0.1 和 15.0.1,行为相同。我什至解雇了 jconsole,堆使用图有一个非常健康的锯状形状,对于这种@ApplicationScoped
情况,内存在每次 GC 后准确地返回到基线。然后,我开始尝试。
我不敢相信 CDI 实际上是在销毁@Dependent
bean 实例,所以我PreDestroy
在Book
. 正如预期的那样,该方法从未被调用,但我开始获得 OOME,即使是@ApplicationScoped
CDI bean!
为什么添加@PostConstruct
一种使应用程序行为不同的方法?我认为正确的问题是相反的,即为什么要删除使@PostConstruct
OOME消失?由于 CDI 必须@Dependent
使用其父对象销毁对象 - 在本例中为Instance<Book>
.,因此它必须@Dependent
在Instance
. 调试一下,你会看到的。该列表是保留对所有已创建@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()
.
推荐阅读
- python - 使用编号规则重命名多个文件夹
- c# - 返回派生泛型类而不指定返回类型的方法
- vue.js - vue beforeRouteEnter 未被调用
- javascript - 如何使用 loadVirtual 和 ENOLOCK 修复 npm 审计错误?
- elasticsearch - 使用 filebeat 的多个分词器
- python - 当给定 ID 前面没有一组值或后面没有其他组值时,删除 df 行
- c# - 连接两个数组时 DynamicExpressionParser 不起作用
- powershell - cmd中的“wmic memorychip get | clip”
- python - 如何包含 Pyinstaller 在生成 .exe 期间无法导入的文件
- python - Youtube Data API V3 请求的身份验证范围不足(Python)