CDI会话作用域bean未销毁会导致内存泄漏



我有一个关于会话作用域CDI bean生命周期的问题。
据我所知,会话作用域的CDI bean是在会话开始时由容器构造的,在会话结束时销毁。在bean被销毁之前,@PreDestroy方法被调用,如下所述https://docs.oracle.com/javaee/6/tutorial/doc/gmgkd.html。它还说在这个方法中释放资源。

在我构建的JSF应用程序中,我经历了内存泄漏,因为bean似乎没有被销毁,因此没有调用@PreDestroy方法来为垃圾收集器释放一些引用。因此,我构建了一个简单的应用程序来测试该行为。我的经验是,会话结束时会话bean不会被销毁,而且它甚至在需要内存空间时也不会被销毁。我不敢相信我是第一个遇到这种情况的人,但是我没有找到任何关于这种行为的信息。

所以我的问题是:CDI bean不应该被销毁-因此@PreDestroy方法被调用-在其上下文过期后立即?如果没有,难道不应该至少在需要空间的时候摧毁它吗?

My test Application:

我不允许张贴图片,但轮廓是由eclipse生成的非常基本的jsf web应用程序。我还有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而没有释放它们之间的空间时,就会超出堆空间。当我释放中间的一个资源,或者在一行中调用一个方法两次时,堆空间就足够了,因为垃圾收集器会释放内存。这是我所期望的工作

但是当我调用一个数据函数时,关闭浏览器和会话,打开一个新的浏览器并再次调用其中一个数据函数,然后应用程序停止工作(我猜)内存空间超过。关键是:第一个会话bean没有被销毁,它的@PreDestroy方法没有被调用,因此ArrayList仍然在内存中。

谁能给我解释一下这是怎么回事?CDI bean的上下文到期后,容器不应该立即销毁它,以便将引用设置为null,垃圾收集器可以释放资源吗?
我正在使用JBoss AS 7.1.1和它的默认实现JSF Mojarra 2.1。

会话bean(无论CDI管理还是JSF管理)在会话超时超过(默认情况下通常为30分钟,取决于应用服务器)之前保持存活,您可以在web.xml中指定该超时。仅仅关闭浏览器并不会使会话失效,它等待servlet容器在超时过期后销毁。所以,我的假设,这样的行为很好,@PreDestroy方法将在稍后调用。

@olexd的答案基本上解释了我脑子里的错误,非常感谢!但是在确定的时间之后使会话无效是不可能的,所以我不得不使用@geert3的评论,谢谢你!我在这里回答我自己的问题,以详细说明我是如何解决我的特殊问题的。

我错了什么:我认为会话过期一旦浏览器关闭。这是错误的,也是有道理的。您可能需要关闭浏览器并重新打开它,以便在与以前相同的会话中工作。
对我来说,这种行为是不合适的,因为我想在浏览器关闭时尽快释放资源。因此,答案是像这样手动使会话无效:

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

一旦这个方法被调用,@PreDestroy方法就会被调用,就像我想要的那样。现在我必须决定何时调用这个函数。我寻找一种方法来收听类似于browserclose事件的内容。有onbeforeunloadonunload事件。onunload在Chrome中似乎不适合我,但onbeforeunload可以。也可以看看这个答案:https://stackoverflow.com/a/16677225/1566562

所以我写了一个隐藏的按钮,通过javascript在beforeunload上点击并调用适当的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实现。
需要补充的一点是:不必手动将所有引用设置为空。这也是有意义的,因为它将是乏味的…

相关内容

  • 没有找到相关文章

最新更新