Внедрение бина @Dependent CDI в EJB вызывает утечку памяти - PullRequest
3 голосов
/ 29 апреля 2020

Тестирование утечек памяти с созданием нескольких экземпляров @Dependent с 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);
    }

}

После создания 200 КБ книги я получаю OutOfMemoryError. Мне это понятно, потому что здесь написано

CDI | Применение / Зависимая сфера применения | Утечка памяти - javax.enterprise.inject.Instance Не собрана сборка мусора

Приложение CDI и зависимые области действия могут сговориться, чтобы повлиять на сборку мусора?

Но У меня есть еще вопросы:

  1. Почему OutOfMemoryError произошла, только если GlobalService в Book является EJB без сохранения состояния, но не если @ApplicationScoped. Я думал, что @ApplicationScoped для GlobalFactory достаточно, чтобы получить OutOfMemoryError.

  2. Какой метод лучше createBook2 () или createBook3 ()? И то, и другое устраняет проблему с OutOfMemoryError

  3. Есть ли другой вариант createBook ()?

1 Ответ

3 голосов
/ 30 апреля 2020

Я был впечатлен и поражен (1). Пришлось попробовать себя, и это действительно так, как вы говорите! Пробовал на WildFly 18.0.1 и 15.0.1, такое же поведение. Я даже запустил jconsole, и график использования кучи имел совершенно здоровую пилообразную форму с памятью, возвращающейся точно к базовой линии после каждого G C, для случая @ApplicationScoped. Затем я начал экспериментировать.

Я не мог поверить, что CDI фактически уничтожает экземпляры бинов @Dependent, поэтому я добавил метод PreDestroy к Book. Метод никогда не вызывался, как ожидалось, но я начал получать OOME, даже для @ApplicationScoped CDI-компонента!

Почему добавление @PostConstruct метода заставляет приложение вести себя по-другому? Я думаю, что правильный вопрос обратный, то есть, почему удаление из @PostConstruct заставляет OOME исчезнуть? Поскольку CDI должен уничтожать @Dependent объекты с их родительским объектом - в данном случае Instance<Book>, он должен хранить список @Dependent объектов внутри Instance. Отладьте, и вы увидите это. Этот список содержит ссылки на все созданные объекты @Dependent и в конечном итоге приводит к утечке памяти. Очевидно (у него не было времени, чтобы найти доказательства) Weld применяет оптимизацию: если объект @Dependent не имеет методов @PostConstruct в своем дереве внедрения зависимостей, Weld не добавляет его в этот список. Вот почему (1) работает (1), когда GlobalService равен @ApplicationScoped.

CDI должен связывать свой жизненный цикл с жизненным циклом EJB при внедрении EJB в bean-компонент CDI. Очевидно (опять же, я предполагаю) CDI создает хук @PostConstruct, когда GlobalService является EJB для связывания двух жизненных циклов. Согласно JSR 365 (CDI 2.0) гл. 18.2:

Сессионный компонент без сохранения состояния должен принадлежать псевдоскопе @Dependent.

Итак, Book получает хук @PostConstruct в своей цепочке из @Dependent объектов:

Book [@Dependent, no @PostConstruct] -> GlobalService [@Dependent, @PostConstruct]

Поэтому Instance<Book> нужна ссылка на каждый Book, который он создает, для вызова метода @PostConstruct ( неявно созданный CDI) зависимого GlobalService EJB.

Разобравшись с тайной (1) (надеюсь), перейдем к (2):

  • createBook2() Недостатком является то, что пользователь должен знать, что целевой бин @Dependent. Если кто-то меняет сферу, уничтожать ее неуместно (если вы действительно не знаете, что делаете). И тогда сохранение ссылки на мертвый экземпляр кажется жутким:)
  • createBook3(): один недостаток состоит в том, что GlobalFactory должен знать зависимости Book. Возможно, это не так уж и плохо, для фабрики разумно, чтобы книги знали свою зависимость. Но тогда вы не получите такие полезные свойства CDI, как @PostConstruct / @PreDestroy, перехватчики для книги (например, транзакции реализованы как перехватчики в CDI). Другим недостатком является то, что простой объект имеет ссылки на компоненты CDI. Если они принадлежат к узкой области (например, @RequestScoped), вы можете хранить ссылки на них за пределами их нормальной продолжительности жизни с непредсказуемыми результатами.

Теперь для (3) и что является лучшим Решение, я думаю, это сильно зависит от вашего конкретного случая использования. Например, если вам нужны все возможности CDI (например, перехватчики) на каждом Book, вы можете отслеживать книги, которые вы создаете вручную, и массово уничтожать, когда это необходимо. Или, если book - это POJO, для которого просто нужно установить свой идентификатор, просто включите go и используйте createBook3().

...