使用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的Book后,出现OutOfMemoryError。 对我来说很清楚,因为它写在这里
CDI Application and Dependent scopes can conspire to impact garbage collection?
但是我还有另一个问题:
为什么仅在Book中的GlobalService是无状态EJB时发生OutOfMemoryError,而在@ApplicationScoped时则不会发生。我认为@ApplicationScoped for GlobalFactory足以获取OutOfMemoryError。
哪种方法更适合createBook2()或createBook3()?两者都消除了OutOfMemoryError问题
答案 0 :(得分:3)
(1)给我留下了深刻的印象和惊奇。不得不尝试一下,确实就是您所说的!在WildFly 18.0.1和15.0.1上尝试过,行为相同。
我什至解雇了jconsole,并且堆使用情况图的形状非常完美,呈锯齿状,在@ApplicationScoped
情况下,每次GC后内存都完全返回基线。
然后,我开始尝试。
我不敢相信CDI实际上正在破坏@Dependent
bean实例,因此我在PreDestroy
上添加了Book
方法。
该方法从未像预期的那样被调用,但是即使对于@ApplicationScoped
CDI bean,我也开始获得OOME!
为什么添加@PostConstruct
方法会使应用程序的行为有所不同?
我认为正确的问题是相反的,即为什么@PostConstruct
的移除使OOME消失了?
由于CDI必须使用其父对象销毁@Dependent
对象-在这种情况下为Instance<Book>
,因此它必须在@Dependent
内保留Instance
对象的列表。
调试,您将看到它。该列表保留了对所有创建的@Dependent
对象的引用,并最终导致内存泄漏。
显然(没有时间找到证据),Weld正在应用优化:如果@Dependent
对象的依赖项注入树中没有@PostConstruct
方法,
Weld并未将其添加到此列表中。
这就是(我的猜测)为什么当GlobalService
为@ApplicationScoped
时(1)可以工作。
在将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()
。