Компонент, ограниченный сеансом CDI, не уничтожается, что приводит к утечке памяти

У меня есть вопрос относительно жизненного цикла сессионных компонентов CDI.
Насколько я понимаю, сессионный компонент CDI в области построения создается контейнером при запуске сеанса и уничтожается при его завершении. Перед уничтожением компонента вызывается метод @PreDestroy, как описано здесь https://docs.oracle.com/javaee/6/tutorial/doc/gmgkd.html. Это также говорит, чтобы выпустить ресурсы в этом методе.

В JSF-приложении, которое я создаю, я испытываю утечку памяти, потому что bean-компонент, кажется, не уничтожен, и, следовательно, метод @PreDestroy не вызывается для освобождения некоторых ссылок для сборщика мусора. Поэтому я создал простое приложение для проверки поведения. Мой опыт показывает, что сессионный компонент не уничтожается при завершении сеанса и, более того, он даже не уничтожается, когда требуется пространство памяти. Я не могу поверить, что я первый столкнулся с этим, но я не нахожу никакой информации об этом поведении..

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

Мое тестовое приложение:

Мне не разрешено публиковать картинку, но набросок - это очень простой jsf webapp, созданный eclipse. У меня также есть файл beans.xml.

Test.java:

package com.test;

import java.io.Serializable;
import java.util.ArrayList;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@SessionScoped
@Named
public class Test implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String test;
    private ArrayList<ComplexType> cps;
    private ArrayList<ComplexType> cps_2;

    @PostConstruct
    public void init() {
        System.out.println("test postconstruct..");
        test = "Cdi Test";
    }

    @PreDestroy
    public void cleanUp() {
        cps = null;
        cps_2 = null;
        System.out.println("test cleanUp....");
    }

    public void data_1() {

        cps = new ArrayList<ComplexType>();

        for(int i = 0; i < 800; i++) {
            String[] s = new String[100000];
            ComplexType cp = new ComplexType(i, s);
            cps.add(cp);
            System.out.println(i);
        }
        System.out.println("data_1");
    }

    public void free_1() {
        cps = null;
        System.out.println("free_1");
    }

    public void data_2() {

        cps_2 = new ArrayList<ComplexType>();

        for(int i = 0; i < 800; i++) {
            String[] s = new String[100000];
            ComplexType cp = new ComplexType(i, s);
            cps_2.add(cp);
            System.out.println(i);
        }
        System.out.println("data_1");
    }

    public void free_2() {
        cps_2 = null;
        System.out.println("free_1");
    }

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }   
}

ComplexType.java:

package com.test;

public class ComplexType {

    private int id;
    private String[] name;

    public ComplexType(int id, String[] name) {

        this.id = id;
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String[] getName() {
        return name;
    }
    public void setName(String[] name) {
        this.name = name;
    }
}

index.xhtml:

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
>

<h:head>
    <title>Cdi test </title>
</h:head>

<h:body>

    <h:outputText value="#{test.test}"></h:outputText>

    <h:form>
        <h:commandButton value="cp_1 data" actionListener="#{test.data_1}">
            <f:ajax></f:ajax>
        </h:commandButton>
        <h:commandButton value="cp_1 Free" actionListener="#{test.free_1}">
            <f:ajax></f:ajax>
        </h:commandButton>

        <br></br>
        <h:commandButton value="cp_2 data" actionListener="#{test.data_2}">
            <f:ajax></f:ajax>
        </h:commandButton>
        <h:commandButton value="cp_2 Free" actionListener="#{test.free_2}">
            <f:ajax></f:ajax>
        </h:commandButton>
    </h:form>

</h:body>
</html>

Я открываю страницу index.xhtml, и метод @PostConstruct вызывается, как и ожидалось. Пространство кучи превышено, когда я вызываю data_1 и data_2 и без освобождения между ними. Когда я освобождаю один из промежуточных ресурсов или вызываю один метод два раза подряд, места в куче достаточно, поскольку сборщик мусора освобождает память. Это работает так, как я ожидал.

Но когда я вызываю одну функцию данных, закрываю браузер и, следовательно, сеанс, открываю новый браузер и снова вызываю одну из функций данных, тогда приложение перестает работать, поскольку (я полагаю) пространство памяти превышено. Дело в том, что первый сессионный компонент не уничтожается, а его метод @PreDestroy не вызывается, и поэтому ArrayList все еще находится в памяти.

Может кто-нибудь объяснить мне, что здесь происходит? Не должен ли bean-компонент CDI быть уничтожен контейнером, как только истечет срок его контекста, так что ссылки могут быть установлены в null, а сборщик мусора сможет освободить ресурсы?
Я использую JBoss AS 7.1.1 и его реализацию по умолчанию JSF Mojarra 2.1.

2 ответа

Решение

Ответ @olexd в основном объясняет, в чем я ошибался, большое спасибо! Но аннулирование сессии после определенного периода не вариант, поэтому мне пришлось использовать комментарий @ geert3, спасибо за это! Я отвечаю на свой вопрос, чтобы показать, как я подробно решил свою конкретную проблему здесь.

В чем я был неправ: я думал, что сессия заканчивается, как только браузер закрывается. Это неправильно и имеет смысл. Можно закрыть браузер и снова открыть его, чтобы работать в том же сеансе, что и раньше.
Для меня это поведение не подходит, потому что я хочу освободить ресурсы, как только браузер закроется. Таким образом, ответ состоит в том, чтобы вручную аннулировать сессию следующим образом:

FacesContext.getCurrentInstance().getExternalContext().invalidateSession();

Как только этот метод вызывается, метод @PreDestroy вызывается именно так, как я этого хочу. Теперь я должен был определить, когда вызывать эту функцию. Я искал способ прослушать что-то вроде события в браузере. Есть события onbeforeunload и onunload. Мне кажется, что onunload не работает для меня в Chrome, но onbeforeunload работает. Смотрите также этот ответ: /questions/4776624/ispolzujte-jquery-ili-onbeforeunload-dlya-ie-i-ff/4776633#4776633

Поэтому я написал скрытую кнопку, которую нажимает javascript перед загрузкой и вызывает соответствующий метод backingbean. Это работает так, как я ожидал. Я протестировал его на Chrome 43.0.2357.65 и IE 11, сейчас я доволен этим. Однако он не работает с onunload, но сейчас это меня не касается.

Так что мой окончательный код нравится это:

index.xhtml

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core">

<h:head>
    <title>Cdi test</title>
    <h:outputScript library="default" name="js/jquery-1.11.3.min.js"
        target="head"></h:outputScript>
</h:head>

<h:body>

    <h:outputText value="#{test.test}"></h:outputText>

    <h:form id="overall">
        <h:commandButton value="cp_1 data" actionListener="#{test.data_1}">
            <f:ajax></f:ajax>
        </h:commandButton>
        <h:commandButton value="cp_1 Free" actionListener="#{test.free_1}">
            <f:ajax></f:ajax>
        </h:commandButton>

        <br></br>
        <h:commandButton value="cp_2 data" actionListener="#{test.data_2}">
            <f:ajax></f:ajax>
        </h:commandButton>
        <h:commandButton value="cp_2 Free" actionListener="#{test.free_2}">
            <f:ajax></f:ajax>
        </h:commandButton>

        <br></br>

        <h:commandButton id="b" style="display:none"
            actionListener="#{test.invalidate}"></h:commandButton>

    </h:form>

    <script type="text/javascript">
        $(window).on('beforeunload', function() {
            $('#overall\\:b').click();
        });
    </script>
</h:body>
</html>

Test.java

package com.test;

import java.io.Serializable;
import java.util.ArrayList;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;

@SessionScoped
@Named
public class Test implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String test;
    private ArrayList<ComplexType> cps;
    private ArrayList<ComplexType> cps_2;

    @PostConstruct
    public void init() {
        System.out.println("test postconstruct..");
        test = "Cdi Test";
    }

    @PreDestroy
    public void cleanUp() {
        cps = null;
        cps_2 = null;
        System.out.println("test cleanUp....");
    }

    public void data_1() {

        cps = new ArrayList<ComplexType>();

        for (int i = 0; i < 800; i++) {
            String[] s = new String[100000];
            ComplexType cp = new ComplexType(i, s);
            cps.add(cp);
            System.out.println(i);
        }
        System.out.println("data_1");
    }

    public void free_1() {
        cps = null;
        System.out.println("free_1");
    }

    public void data_2() {

        cps_2 = new ArrayList<ComplexType>();

        for (int i = 0; i < 800; i++) {
            String[] s = new String[100000];
            ComplexType cp = new ComplexType(i, s);
            cps_2.add(cp);
            System.out.println(i);
        }
        System.out.println("data_2");
    }

    public void free_2() {
        cps_2 = null;
        System.out.println("free_2");
    }

    public void invalidate() {
        FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
        System.out.println("invalidate");
    }

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }

}

Обратите внимание, что я использовал JQuery. Это работает с JBoss AS 7.1.1 и реализацией Weld по умолчанию.
Одна вещь, которую нужно добавить: не нужно вручную устанавливать все ссылки на null. Это имеет смысл, так как это было бы утомительно..

Сессионные компоненты (независимо от того, управляется ли CDI или JSF) остаются в силе, пока не истечет некоторое время ожидания сеанса (обычно 30 минут по умолчанию, в зависимости от сервера приложений), которое можно указать в файле web.xml. Простое закрытие браузера не делает сессию недействительной, и он ожидает уничтожения контейнером сервлета после истечения времени ожидания. Итак, я предполагаю, что такое поведение просто отлично, метод @PreDestroy будет вызван позже.

Другие вопросы по тегам