【问题标题】:CDI session scoped bean not destroyed results in memory leakageCDI 会话范围 bean 未销毁导致内存泄漏
【发布时间】:2024-01-20 09:48:01
【问题描述】:

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

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

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

我的测试应用:

不允许发图,但是大纲是eclipse生成的非常基础的jsf webapp。我也有 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。

【问题讨论】:

  • JBoss AS 7.1.1 是古老的。至少尝试当前的 Weld 版本,以排除已知且早已修复的错误。
  • 好的,谢谢BalusC,我会试试然后回来!
  • 我已将 WELD 实现升级到 1.1.23,但没有帮助。
  • 之后我得到了wildfly 8.2.0并尝试在那里运行应用程序,但它与上面的jboss AS 7.1.1相同。它抛出一个 Java 堆空间 OutOfMemoryError
  • 我只是想知道:我的想法错了吗?或者我是否正确假设 bean 应该在上下文到期后立即销毁?

标签: jsf cdi destroy weld session-scope


【解决方案1】:

会话 bean(不管 CDI 或 JSF 托管)一直保持活动状态,直到超过某些会话超时(默认情况下通常为 30 分钟,取决于应用程序服务器),您可以在 web.xml 中指定。只是关闭浏览器不会使会话无效,它会在超时到期后等待被 servlet 容器销毁。所以,我的假设,这样的行为很好,@PreDestroy 方法稍后会被调用。

【讨论】:

  • 谢谢 olexd,你是对的。我在 web.xml 中设置了以下内容:&lt;session-config&gt; &lt;session-timeout&gt;1&lt;/session-timeout&gt; &lt;/session-config&gt; 并在一分钟后销毁 bean 并调用 @PreDestroy 方法
【解决方案2】:

@olexd 的回答基本说明了我脑子里的问题,非常感谢!但是在确定的时间段后使会话无效不是一种选择,所以我也不得不使用@geert3 的评论,谢谢!我正在回答我自己的问题,以展示我如何在此处详细解决我的特定问题。

我错了:我认为浏览器关闭后会话就会过期。这是错误的,这是有道理的。您可能希望关闭浏览器并再次打开它以在同一个会话中工作。
对我来说,这种行为是不合适的,因为我想在浏览器关闭后立即释放资源。所以答案是像这样手动使会话无效:

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

只要调用了这个方法,就会调用 @PreDestroy 方法,这正是我想要的。现在我必须确定何时调用此函数。我寻找一种方法来收听诸如 browserclose 事件之类的东西。有 onbeforeunloadonunload 事件。 onunload 在 Chrome 中似乎对我不起作用,但 onbeforeunload 可以。另请参阅此答案: https://*.com/a/16677225/1566562

所以我写了一个隐藏按钮,在 beforeunload 上被 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 实现。
要添加的一件事:不必手动将所有引用设置为空。这也很有意义,因为它会很乏味..

【讨论】: