【问题标题】:OutOfMemoryError as a result of multiple searches多次搜索导致 OutOfMemoryError
【发布时间】:2015-02-08 07:49:12
【问题描述】:

我有一个经典的 Java EE 系统,带有 JSF 的 Web 层,用于 BL 的 EJB 3,以及对 DB2 数据库进行数据访问的 Hibernate 3。我在以下场景中苦苦挣扎:用户将启动一个涉及从数据库中检索大型数据集的过程。检索过程需要一些时间,因此用户不会立即收到响应,变得不耐烦并打开新浏览器并再次启动检索,有时会多次启动。 EJB 容器显然没有意识到第一次检索不再相关这一事实,当数据库返回结果集时,Hibernate 开始填充一组占用大量内存的 POJO,最终导致OutOfMemoryError

我想到的一个潜在解决方案是使用 Hibernate Session 的 cancelQuery 方法。但是,cancelQuery 方法仅在数据库返回结果集之前有效。一旦数据库返回结果集并且 Hibernate 开始填充 POJO,cancelQuery 方法将不再有效。在这种情况下,数据库查询本身返回得相当快,并且大部分性能开销似乎都存在于填充 POJO 中,此时我们不能再调用 cancelQuery 方法。

【问题讨论】:

    标签: java hibernate jakarta-ee db2 out-of-memory


    【解决方案1】:

    最终实现的解决方案如下所示:

    一般的想法是维护当前正在运行查询的所有 Hibernate 会话到发起它们的用户的 HttpSession 的映射,这样当用户关闭浏览器时,我们就能够终止正在运行的查询。

    这里有两个主要挑战需要克服。一种是将 HTTP session-id 从 Web 层传播到 EJB 层,而不干扰沿途的所有方法调用——即不篡改系统中的现有代码。第二个挑战是弄清楚如何在数据库已经开始返回结果并且 Hibernate 正在用结果填充对象时取消查询。

    第一个问题得到了解决,因为我们认识到沿堆栈调用的所有方法都由同一个线程处理。这是有道理的,因为我们的应用程序都存在于一个容器和does not have any remote calls 中。既然如此,我们创建了一个 Servlet 过滤器,它拦截对应用程序的每次调用,并添加一个带有当前 HTTP 会话 ID 的 ThreadLocal 变量。这样一来,HTTP session-id 将可用于沿线下方的每个方法调用。

    第二个挑战有点棘手。我们发现负责运行查询并随后填充 POJO 的 Hibernate 方法称为 doQuery 并位于 org.hibernate.loader.Loader.java 类中。 (我们碰巧使用的是 Hibernate 3.5.3,但新版本的 Hibernate 也是如此。):

    private List doQuery(
            final SessionImplementor session,
            final QueryParameters queryParameters,
            final boolean returnProxies) throws SQLException, HibernateException {
    
        final RowSelection selection = queryParameters.getRowSelection();
        final int maxRows = hasMaxRows( selection ) ?
                selection.getMaxRows().intValue() :
                Integer.MAX_VALUE;
    
        final int entitySpan = getEntityPersisters().length;
    
        final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 );
        final PreparedStatement st = prepareQueryStatement( queryParameters, false, session );
        final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session );
    
        final EntityKey optionalObjectKey = getOptionalObjectKey( queryParameters, session );
        final LockMode[] lockModesArray = getLockModes( queryParameters.getLockOptions() );
        final boolean createSubselects = isSubselectLoadingEnabled();
        final List subselectResultKeys = createSubselects ? new ArrayList() : null;
        final List results = new ArrayList();
    
        try {
    
            handleEmptyCollections( queryParameters.getCollectionKeys(), rs, session );
    
            EntityKey[] keys = new EntityKey[entitySpan]; //we can reuse it for each row
    
            if ( log.isTraceEnabled() ) log.trace( "processing result set" );
    
            int count;
            for ( count = 0; count < maxRows && rs.next(); count++ ) {
    
                if ( log.isTraceEnabled() ) log.debug("result set row: " + count);
    
                Object result = getRowFromResultSet( 
                        rs,
                        session,
                        queryParameters,
                        lockModesArray,
                        optionalObjectKey,
                        hydratedObjects,
                        keys,
                        returnProxies 
                );
                results.add( result );
    
                if ( createSubselects ) {
                    subselectResultKeys.add(keys);
                    keys = new EntityKey[entitySpan]; //can't reuse in this case
                }
    
            }
    
            if ( log.isTraceEnabled() ) {
                log.trace( "done processing result set (" + count + " rows)" );
            }
    
        }
        finally {
            session.getBatcher().closeQueryStatement( st, rs );
        }
    
        initializeEntitiesAndCollections( hydratedObjects, rs, session, queryParameters.isReadOnly( session ) );
    
        if ( createSubselects ) createSubselects( subselectResultKeys, queryParameters, session );
    
        return results; //getResultList(results);
    
    }
    

    在此方法中,您可以看到首先以老式java.sql.ResultSet 的形式从数据库中获取结果,然后在每个集合上循环运行并从中创建一个对象。在循环之后调用的initializeEntitiesAndCollections() 方法中执行了一些额外的初始化。稍作调试后,我们发现大部分性能开销都在方法的这些部分,而不是从数据库中获取java.sql.ResultSet的部分,但cancelQuery方法仅在第一部分有效.因此,解决方案是在 for 循环中添加一个附加条件,以检查线程是否被中断,如下所示:

    for ( count = 0; count < maxRows && rs.next() && !currentThread.isInterrupted(); count++ ) {
    // ...
    }
    

    以及在调用initializeEntitiesAndCollections() 方法之前执行相同的检查:

    if (!Thread.interrupted()) {
    
        initializeEntitiesAndCollections(hydratedObjects, rs, session,
                    queryParameters.isReadOnly(session));
        if (createSubselects) {
    
            createSubselects(subselectResultKeys, queryParameters, session);
        }
    }
    

    另外,通过在第二次检查中调用Thread.interrupted(),标志被清除并且不影响程序的进一步运行。现在,当要取消查询时,取消方法会访问存储在映射中的 Hibernate 会话和线程,以 HTTP 会话 ID 作为键,调用会话上的 cancelQuery 方法并调用会话的 interrupt 方法线程。

    【讨论】:

      【解决方案2】:

      我在完全不同的环境中遇到了类似的问题。我做了以下事情:在将新作业添加到我的队列之前,我首先检查了该用户是否已将“相同作业”排入队列。如果是这样,我不接受第二份工作并通知用户。

      如果数据太大而无法放入可用内存中,这并不能回答您关于如何保护用户免受内存不足的问题。但这是一个很好的技巧,可以保护您的服务器不做无用的事情。

      【讨论】:

        【解决方案3】:

        对我来说太复杂了 :-) 我想为“繁重”的查询创建单独的服务。并在其中存储有关查询参数的信息,也许是结果,这将在有限的时间内有效。如果查询执行时间过长,用户会收到消息,他的任务执行将花费相当长的时间,他可能会等待或取消它。这种情况适用于分析查询。这个变体让你可以简单地访问在服务器上运行的任务,以杀死它。

        但是,如果您遇到休眠问题,那么我认为问题不在分析查询中,而是在普通业务查询中。如果执行时间过长,是否可以尝试使用二级缓存(冷启动可能很长,但会立即收到热数据)?或者优化hibernate\jbdc参数?

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-07-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-20
          相关资源
          最近更新 更多