Injecting @Dependent CDI bean into EJB causes memory leak
Asked Answered
H

1

5

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()?
Hoitytoity answered 28/4, 2020 at 21:38 Comment(0)
S
6

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

Sennet answered 29/4, 2020 at 21:6 Comment(3)
From CDI I need only injection and initializing, like Guice. No need to call PreDestroy. Books after creation stored in cache, and I don't know when they will be removed from cache or stop used in other places.Hoitytoity
@Hoitytoity in your case I wouldn't keep any references to services in the book. I would try to run the initialization logic outside the bean, e.g. a CDI producer method. Your GlobalFactory.createBookN() functions are excellent candidates.Sennet
Checked that any Instance<STATELESS_BEAN>.get creates org.jboss.weld.contexts.SerializableContextualInstanceImpl. If I call GlobalService (when it @Stateless) multiple times from GlobalSingleton it leads to OutOfMemoryError. While I have 1 instance of GlobalSingleton and 5 instances of GlobalService.Hoitytoity

© 2022 - 2024 — McMap. All rights reserved.