Компонент, ограниченный сеансом 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 будет вызван позже.