使用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 and Dependent scopes can conspire to impact garbage collection?
但是我还有另一个问题:
为什么只有在Book中的GlobalService是无状态EJB时才会发生OutOfMemoryError,而如果@ApplicationScoped则不会。我认为@ApplicationScoped for GlobalFactory足以获取OutOfMemoryError。
哪种方法更好的createBook2()或createBook3()?两者都消除了OutOfMemoryError的问题
我对(1)印象深刻并感到惊讶。不得不尝试一下,确实就是您所说的!在WildFly 18.0.1和15.0.1上尝试过,行为相同。我什至解雇了jconsole,对于@ApplicationScoped
情况,堆使用情况图具有非常健康的锯齿状形状,在每次GC之后,内存都完全返回到基线。然后,我开始尝试。
我不敢相信CDI实际上正在破坏@Dependent
bean实例,因此我向PreDestroy
添加了Book
方法。该方法从未像预期的那样被调用,但是即使对于@ApplicationScoped
CDI bean,我也开始获得OOME!
为什么添加@PostConstruct
方法会使应用程序的行为有所不同?我认为正确的问题是相反的,即为什么@PostConstruct
的removal使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()
。