Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Injecting @Dependent CDI bean into EJB causes memory leak

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:

  1. 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.

  2. What method better createBook2() or createBook3()? Both remove problem with OutOfMemoryError

  3. Is there other variant of createBook()?
like image 330
Rustam Avatar asked Apr 28 '20 21:04

Rustam


People also ask

What are the causes of memory leaks?

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.

What are memory leaks?

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.

Does CDI inject the injected bean into another resource?

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?

What is the difference between EJB and CDI managed beans?

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!

How do I inject all dependencies into a managed bean?

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.

How to perform dependency injection in Java EE CDI?

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


1 Answers

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().

like image 53
Nikos Paraskevopoulos Avatar answered Nov 15 '22 10:11

Nikos Paraskevopoulos