Reputation: 1407
Testing memory leaks with creation of multiple @Dependent instances with WildFly 18.0.1
@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);
}
}
After creating 200k of Book I get the OutOfMemoryError. It's clear to me because it is written here
CDI Application and Dependent scopes can conspire to impact garbage collection?
But I have another questions:
Why OutOfMemoryError occurred only if GlobalService in Book is stateless EJB, but not if @ApplicationScoped. I thought that @ApplicationScoped for GlobalFactory is enough to get OutOfMemoryError.
What method better createBook2() or createBook3()? Both remove problem with OutOfMemoryError
Upvotes: 5
Views: 1176
Reputation: 40298
I was impressed and amazed by (1). Had to try myself and indeed it is exactly as you say! Tried on a WildFly 18.0.1 and a 15.0.1, same behavior.
I even fired jconsole and the heap usage graph had a perfectly healthy saw-like shape, with memory returning exactly to the baseline after each GC, for the @ApplicationScoped
case.
Then, I started experimenting.
I could not believe that CDI was actually destroying the @Dependent
bean instances, so I added a PreDestroy
method to the Book
.
The method was never called, as expected, but I started getting the OOME, even for an @ApplicationScoped
CDI bean!
Why is the addition of a @PostConstruct
method making the application behave differently?
I think the correct question is the inverse, i.e. why is the removal of the @PostConstruct
making the OOME disappear?
Since CDI has to destroy @Dependent
objects with their parent object - in this case the Instance<Book>
, it has to keep a list of @Dependent
objects inside the Instance
.
Debug, and you will see it. This list is the one keeping the references to all the created @Dependent
objects and ultimately leads to the memory leak.
Apparently (did't have time to find evidence) Weld is applying an optimization: if a @Dependent
object does not have @PostConstruct
methods in its dependency injection tree,
Weld is not adding it to this list.
That is (my guess) why (1) works when the GlobalService
is @ApplicationScoped
.
CDI has to bind its own lifecycle with the EJB lifecycle, when injecting an EJB to a CDI bean.
Apparently (again, my guess) CDI is creating a @PostConstruct
hook when GlobalService
is an EJB to bind the two lifecycles.
According to JSR 365 (CDI 2.0) ch 18.2:
A stateless session bean must belong to the
@Dependent
pseudo-scope.
So, the Book
acquires a @PostConstruct
hook in its chain of @Dependent
objects:
Book [@Dependent, no @PostConstruct] -> GlobalService [@Dependent, @PostConstruct]
Therefore the Instance<Book>
needs a reference to every Book
it creates, in order to call the @PostConstruct
method (created implicitly by CDI) of the dependent GlobalService
EJB.
Having solved the mystery of (1) (hopefully) let's move on to (2):
createBook2()
: The disadvantage is that the user has to know that the target bean is @Dependent
. If someone changes the scope, then destroying it is inappropriate (unless you really know what you are doing). And then keeping around a reference to a dead instance seems creepy :)createBook3()
: One disadvantage is that the GlobalFactory
has to know the dependencies of Book
. Perhaps that is not too bad, it is reasonable for a factory for books to know their dependencies. But then, you do not get the CDI goodies like @PostConstruct
/@PreDestroy
, interceptors for a book (e.g. transactions are implemented as interceptors in CDI). Another disadvantage is that a plain object has references to CDI beans. If these are belong to a narrow scope (e.g. @RequestScoped
), you might be keeping references to them beyond their normal lifespan, with unpredictable results.Now for (3) and what is the best solution, I think it strongly depends on your exact use case. E.g. if you want the full CDI facilities (e.g. interceptors) on each Book
, you may want to keep track of the books you create manually, and bulk-destroy when appropriate. Or, if book is a POJO that just needs its id to be set, you just go on and use createBook3()
.
Upvotes: 6