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 / Dependent Scope | Memory Leak - javax.enterprise.inject.Instance<T> Not Garbage Collected
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
Memory leaks are a common error in programming, especially when using languages that have no built in automatic garbage collection, such as C and C++. Typically, a memory leak occurs because dynamically allocated memory has become unreachable.
DEFINITION A memory leak is the gradual deterioration of system performance that occurs over time as the result of the fragmentation of a computer's RAM due to poorly designed or programmed applications that fail to free up memory segments when they are no longer needed.
When we inject a managed bean that is created in a scope different than @Dependent - into another managed resource - the CDI container does not inject a direct reference to the injected bean. For CDI bean scopes please see Java EE CDI bean scopes. Why does CDI uses proxies?
Notably, a CDI managed bean is anything that can be @Inject ed into another CDI bean and can itself use @Inject, which is true for all EJBs, and @EJB can be used to inject an EJB into any other EE managed bean (EJB, servlet, CDI managed bean, etc.). Thanks for contributing an answer to Stack Overflow!
We may inject them all into a managed bean using the @Any qualifier along with the CDI Instance interface: The @Any qualifier instructs the container that this injection point may be satisfied by any available dependency, so the container injects them all.
Java EE CDI makes primarily use of the @Inject annotation in order to perform Dependency Injection of managed beans into other container managed resources. In this tutorial we will cover the different available strategies to perform dependency injection in a CDI environment. This tutorial considers the following environment: JDK 1.7.0.21
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()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With