Hibernate、SessionFactoryObjectFactory和OutOfMemoryError:java heap space

7
在我工作的地方,我们的一个应用程序出现了JVM堆空间不足的问题。我已经搜索了一些原因,包括使用分析器查看堆转储文件,但现在我基本上陷入了困境。
首先,让我们来了解一下相关系统:这是一个Java应用程序,使用Spring和Hibernate来记录组织机构的信息。该系统由一组Web服务客户端组成,用于从负责此类数据的政府机构检索有关组织机构的数据。此外,该系统还保留了一个本地数据库,用于缓存Web服务调用的数据,以便在首次请求组织机构信息时将其保存在本地关系型数据库中,并在随后的请求中用于检索数据。Hibernate用于与此数据库通信。
正如之前所述,问题在于经过一段时间后,应用程序开始崩溃并显示OutOfMemoryError:java heap space。我使用Eclipse+MAT查看了一个堆转储文件,并确定罪魁祸首是Hibernate的SessionFactoryObjectFactory,它占用了大约85%的分配内存(所有保留内存)。我发现很难确定在其中保存了哪种类型的对象。在顶层,有Glassfish WebappClassLoader,其中包含org.hibernate.impl.SessionFactoryObjectFactory。其中包含一个org.hibernate.util.FastHashMap,它又包含一个java.util.HashMap。这包含多个条目,每个条目都包含一个HashMap条目、一个SessionFactoryImpl和一个字符串。HashMap条目又包含相同的三个对象:HashMap条目、SessionFactoryImpl和一个字符串,并且此结构重复多次。SessionFactoryImpl包含许多对象,最重要的是org.hibernate.persister.entity.SingleTableEntityPersister,其中包含许多字符串和HashMap。一些字符串引用域对象中的变量,而另一些包含SQL语句。
乍一看,似乎这个对象占用了不必要的大量内存(转储文件大小为800MB,其中650MB被SessionFactoryObjectFactory占用),因此我启用了对象加载和卸载的日志记录,并尝试通过另一个系统的Web服务调用请求组织机构的数据。我在这里注意到有很多加载对象的消息,但很少有关于卸载对象的消息(唯一的消息是卸载库对象)。这使我相信,一旦将对象(例如组织机构)加载到内存中,它就永远不会被卸载,这意味着随着时间的推移,系统将耗尽内存。(基于日志发现的情况,这是一个公正的假设吗?)
然后,我试图找出其中的原因,但这很困难。由于Hibernate加载的对象将与其会话存活一样久,所以我尝试改变处理会话的方式,通过替换对Spring的HibernateDaoSupport#getSession()的调用为HibernateDaoSupport#getSessionFactory().getCurrentSession()。这对问题没有明显影响。我还尝试在一些相关Dao方法的finally块中添加对getCurrentSession().flush().clear()的调用,同样没有明显效果。 (所有Dao方法都带有@Transactional注释,这应该意味着会话只应在@Transactional方法内存在,并且在调用getCurrentSession()时应获取不同的会话吗?)
因此,现在我几乎无法找到其他需要检查的区域。有人有想法或指针可以告诉我在哪里查找以及要查找什么吗?
堆转储显示,有许多org.hibernate.impl.SessionFactoryImpl实例,这是否符合预期? (我认为应该只有一个SessionFactory实例,或者最多只有几个。)
编辑:
我认为我实际上已经解决了这个问题:
事实证明,处理webservice类中其他对象的依赖项是问题所在。这通过在webservice类的构造函数中调用newClassPathXmlApplicationContext(...)来解决。这导致每个请求(或至少每个会话)加载了大量对象,而这些对象没有卸载(主要是Hibernate的SessionFactoryImpl)。我已将webservice类更改为注入其依赖项,并根据我使用的分析工具所看到的形式,多个SessionFactoryImpl对象的问题已得到解决。
我认为升级从GlassFish 2.x到GlassFish 3.x可能会加剧问题,可能是关于如何实例化webservice类的差异。
1个回答

5
我可以在回答中添加这个问题的解决方案,而不仅仅是在问题本身中:
罪魁祸首在于各种对象中加载spring-beans的方式,尤其是在webservice类中。这是通过调用new ClassPathXmlApplicationContext(...)来完成的。
在个别的webservice类中,这样做会产生一个恶劣的副作用,即已加载的对象避免被垃圾收集(我猜可能是因为它们被Spring的某些内部引用)。似乎Glassfish版本的更改导致了对webservice对象实例化的更多调用,从而导致大量垃圾对象占用内存,直到内存被填满并崩溃。
解决问题的方法是将对new ClassPathXmlApplicationContext(...)的调用移出到一个不同的类中,使用静态工厂模式,像这样:
public class ContextHolder {
    private static ClassPathXmlApplicationContext context;

    public static getSpringContext() {
        if (context == null) {
            context = new ClassPathXmlApplicationContext("applicationContext.xml");
        }
        return context;
    }
}

在Web服务类中调用此方法,而不是使用new来创建ClassPathXmlApplicationContext。

更新:

ClassPathXmlApplicationContext 是可关闭的(Closeable) / 自动关闭的(Autocloseable),因此还有一种可能性是使用 try-with-resource

try (final ClassPathXmlApplicationContext applicationContext =
             new ClassPathXmlApplicationContext("applicationContext.xml")) {
    //do stuff
}

除了静态工厂之外,我认为ClassPathXmlApplicationContext有一个close方法可以调用来避免这个问题。 - Tobb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接