OutOfMemoryError 作为多次搜索的结果
OutOfMemoryError as a result of multiple searches
我有一个经典的 Java EE 系统,带有 JSF 的 Web 层,用于 BL 的 EJB 3,以及对 DB2 数据库进行数据访问的 Hibernate 3。我在以下场景中苦苦挣扎:用户将启动一个过程,该过程涉及从数据库中检索大型数据集。检索过程需要一些时间,因此用户没有立即收到响应,不耐烦并打开新浏览器并再次启动检索,有时是多次。 EJB 容器显然没有意识到第一次检索不再相关的事实,并且当数据库 return 是结果集时,Hibernate 开始填充一组占用大量内存的 POJO,最终导致 OutOfMemoryError
.
我想到的一个潜在解决方案是使用 Hibernate Session 的 cancelQuery
方法。但是,cancelQuery
方法仅在 数据库 return 成为结果集之前 有效。一旦数据库 return 成为结果集并且 Hibernate 开始填充 POJO,cancelQuery
方法就不再有效。在这种情况下,数据库查询自身 return 相当快,并且大部分性能开销似乎存在于填充 POJO 中,此时我们不能再调用 cancelQuery
方法。
我在完全不同的环境中遇到了类似的问题。我做了以下操作:在将新作业添加到我的队列之前,我首先检查 'same job' 是否已经从该用户入队。如果是这样我不接受第二份工作并告知用户。
这没有回答您关于在数据太大而无法放入可用 ram 时如何保护用户免受内存不足的问题。但这是保护您的服务器不做无用事情的好方法。
实施的解决方案最终如下所示:
一般的想法是维护当前 运行 查询到发起它们的用户的 HttpSession 的所有 Hibernate 会话的映射,这样当用户关闭浏览器时我们就可以终止 运行 查询。
这里有两个主要挑战需要克服。一个是将 HTTP session-id 从 Web 层传播到 EJB 层,而不干扰沿途的所有方法调用——即不篡改系统中的现有代码。第二个挑战是弄清楚一旦数据库已经开始返回结果并且 Hibernate 正在用结果填充对象时如何取消查询。
第一个问题已经解决,因为我们意识到沿堆栈调用的所有方法都由同一个线程处理。这是有道理的,因为我们的应用程序都存在于一个容器中 does not have any remote calls。在这种情况下,我们创建了一个 Servlet 过滤器,它拦截对应用程序的每个调用,并添加一个 ThreadLocal
变量和当前的 HTTP 会话 ID。这样,HTTP 会话 ID 将可用于沿线下方的每个方法调用。
第二个挑战有点棘手。我们发现负责 运行 查询并随后填充 POJO 的 Hibernate 方法被称为 doQuery
并且位于 org.hibernate.loader.Loader.java
class 中。 (我们碰巧使用的是 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()
,标志被清除并且不影响程序的进一步运行。现在,当要取消查询时,取消方法访问存储在以 HTTP session-id 为键的映射中的 Hibernate 会话和线程,调用会话上的 cancelQuery
方法并调用 interrupt
线程的方法。
对我来说太复杂了 :-) 我想为 "heavy" 查询创建单独的服务。并在其中存储有关查询参数的信息,可能是结果,这将是有效的有限时间。如果查询执行时间太长,用户会收到消息,执行他的任务将花费相当长的时间,他可能会等待或取消它。这种情况适用于分析查询。此变体使您可以简单地访问服务器上的任务 运行 以终止它。
但是,如果您遇到休眠问题,那么我认为问题不在分析查询中,而是在普通业务查询中。如果执行时间过长,可否尝试使用L2缓存(冷启动可能会很长,但热数据会立即收到)?或者优化hibernate\jbdc个参数?
我有一个经典的 Java EE 系统,带有 JSF 的 Web 层,用于 BL 的 EJB 3,以及对 DB2 数据库进行数据访问的 Hibernate 3。我在以下场景中苦苦挣扎:用户将启动一个过程,该过程涉及从数据库中检索大型数据集。检索过程需要一些时间,因此用户没有立即收到响应,不耐烦并打开新浏览器并再次启动检索,有时是多次。 EJB 容器显然没有意识到第一次检索不再相关的事实,并且当数据库 return 是结果集时,Hibernate 开始填充一组占用大量内存的 POJO,最终导致 OutOfMemoryError
.
我想到的一个潜在解决方案是使用 Hibernate Session 的 cancelQuery
方法。但是,cancelQuery
方法仅在 数据库 return 成为结果集之前 有效。一旦数据库 return 成为结果集并且 Hibernate 开始填充 POJO,cancelQuery
方法就不再有效。在这种情况下,数据库查询自身 return 相当快,并且大部分性能开销似乎存在于填充 POJO 中,此时我们不能再调用 cancelQuery
方法。
我在完全不同的环境中遇到了类似的问题。我做了以下操作:在将新作业添加到我的队列之前,我首先检查 'same job' 是否已经从该用户入队。如果是这样我不接受第二份工作并告知用户。
这没有回答您关于在数据太大而无法放入可用 ram 时如何保护用户免受内存不足的问题。但这是保护您的服务器不做无用事情的好方法。
实施的解决方案最终如下所示:
一般的想法是维护当前 运行 查询到发起它们的用户的 HttpSession 的所有 Hibernate 会话的映射,这样当用户关闭浏览器时我们就可以终止 运行 查询。
这里有两个主要挑战需要克服。一个是将 HTTP session-id 从 Web 层传播到 EJB 层,而不干扰沿途的所有方法调用——即不篡改系统中的现有代码。第二个挑战是弄清楚一旦数据库已经开始返回结果并且 Hibernate 正在用结果填充对象时如何取消查询。
第一个问题已经解决,因为我们意识到沿堆栈调用的所有方法都由同一个线程处理。这是有道理的,因为我们的应用程序都存在于一个容器中 does not have any remote calls。在这种情况下,我们创建了一个 Servlet 过滤器,它拦截对应用程序的每个调用,并添加一个 ThreadLocal
变量和当前的 HTTP 会话 ID。这样,HTTP 会话 ID 将可用于沿线下方的每个方法调用。
第二个挑战有点棘手。我们发现负责 运行 查询并随后填充 POJO 的 Hibernate 方法被称为 doQuery
并且位于 org.hibernate.loader.Loader.java
class 中。 (我们碰巧使用的是 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()
,标志被清除并且不影响程序的进一步运行。现在,当要取消查询时,取消方法访问存储在以 HTTP session-id 为键的映射中的 Hibernate 会话和线程,调用会话上的 cancelQuery
方法并调用 interrupt
线程的方法。
对我来说太复杂了 :-) 我想为 "heavy" 查询创建单独的服务。并在其中存储有关查询参数的信息,可能是结果,这将是有效的有限时间。如果查询执行时间太长,用户会收到消息,执行他的任务将花费相当长的时间,他可能会等待或取消它。这种情况适用于分析查询。此变体使您可以简单地访问服务器上的任务 运行 以终止它。
但是,如果您遇到休眠问题,那么我认为问题不在分析查询中,而是在普通业务查询中。如果执行时间过长,可否尝试使用L2缓存(冷启动可能会很长,但热数据会立即收到)?或者优化hibernate\jbdc个参数?