将@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?
但我还有一个问题:
为什么OutOfMemoryError只发生在Book中的GlobalService是stateless EJB,而不是@ApplicationScoped。我认为 GlobalFactory 的 @ApplicationScoped 足以导致 OutOfMemoryError。
createBook2() 或 createBook3() 哪种方法更好?两者都解决了 OutOfMemoryError
的问题
- 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()
.
使用 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?
但我还有一个问题:
为什么OutOfMemoryError只发生在Book中的GlobalService是stateless EJB,而不是@ApplicationScoped。我认为 GlobalFactory 的 @ApplicationScoped 足以导致 OutOfMemoryError。
createBook2() 或 createBook3() 哪种方法更好?两者都解决了 OutOfMemoryError
的问题
- 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
.
在将 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()
.