【问题标题】:OutOfMemory when reading big amounts of data using hibernate使用休眠读取大量数据时出现内存不足
【发布时间】:2023-03-17 16:30:01
【问题描述】:

我需要从数据库中导出大量数据。这是代表我的数据的类:

public class Product{
...

    @OneToMany
    @JoinColumn(name = "product_id")
    @Cascade({SAVE_UPDATE, DELETE_ORPHAN})
    List<ProductHtmlSource> htmlSources = new ArrayList<ProductHtmlSource>();

... }

ProductHtmlSource - 包含我实际需要导出的大字符串。

由于导出数据的大小大于 JVM 内存,因此我按块读取数据。像这样:

final int batchSize = 1000;      
for (int i = 0; i < 50; i++) {
  ScrollableResults iterator = getProductIterator(batchSize * i, batchSize * (i + 1));
  while (iterator.getScrollableResults().next()) {
     Product product = (Product) iterator.getScrollableResults().get(0); 
     List<String> htmls = product.getHtmlSources();
     <some processing>
  }

}

getProductIterator的代码:

public ScrollableResults getProductIterator(int offset, int limit) {
        Session session = getSession(true);
        session.setCacheMode(CacheMode.IGNORE);
        ScrollableResults iterator = session
                .createCriteria(Product.class)
                .add(Restrictions.eq("status", Product.Status.DONE))
                .setFirstResult(offset)
                .setMaxResults(limit)
                .scroll(ScrollMode.FORWARD_ONLY);
        session.flush();
        session.clear();

        return iterator;
    }

问题是,尽管我在读取每个数据块后清除会话 Product 对象在某处累积并且我得到 OutOfMemory 异常。问题不在于处理代码块,即使没有它我也会遇到内存错误。批处理的大小也不是问题,因为 1000 个对象很容易进入内存。

Profiler 显示对象在 org.hibernate.engine.StatefulPersistenceContext 类中累积。

堆栈跟踪:

Caused by: java.lang.OutOfMemoryError: Java heap space
    at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:99)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:518)
    at java.lang.StringBuffer.append(StringBuffer.java:307)
    at org.hibernate.type.TextType.get(TextType.java:41)
    at org.hibernate.type.NullableType.nullSafeGet(NullableType.java:163)
    at org.hibernate.type.NullableType.nullSafeGet(NullableType.java:154)
    at org.hibernate.type.AbstractType.hydrate(AbstractType.java:81)
    at org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:2101)
    at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1380)
    at org.hibernate.loader.Loader.instanceNotYetLoaded(Loader.java:1308)
    at org.hibernate.loader.Loader.getRow(Loader.java:1206)
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:580)
    at org.hibernate.loader.Loader.doQuery(Loader.java:701)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:236)
    at org.hibernate.loader.Loader.loadCollection(Loader.java:1994)
    at org.hibernate.loader.collection.CollectionLoader.initialize(CollectionLoader.java:36)
    at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:565)
    at org.hibernate.event.def.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:63)
    at org.hibernate.impl.SessionImpl.initializeCollection(SessionImpl.java:1716)
    at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:344)
    at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:86)
    at org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:109)
    at org.hibernate.collection.PersistentBag.size(PersistentBag.java:225)
    **at com.rivalwatch.plum.model.Product.getHtmlSource(Product.java:76)
    at com.rivalwatch.plum.model.Product.getHtmlSourceText(Product.java:80)
    at com.rivalwatch.plum.readers.AbstractDataReader.getData(AbstractDataReader.java:64)**

【问题讨论】:

  • 已发布堆栈跟踪,但我认为调整 gc 不会有帮助。我试过 System.gc();在读取新批次之前,内存仍然溢出。

标签: java hibernate orm out-of-memory batch-processing


【解决方案1】:

您可以发布异常堆栈跟踪吗? 可以通过为 GC 传递合适的 JVM 选项来解决。

我认为这是相关的 - Java StringBuilder huge overhead

从 StackTrace 中可以看出正在创建一个非常大的字符串并导致异常。

【讨论】:

    【解决方案2】:

    冒着显得愚蠢的风险 - 您是否考虑过以另一种方式这样做?

    就我个人而言,我会避免在“远离”数据库的地方进行批处理。我不知道您使用的是什么数据库,但通常有一种机制可以有效地将数据集从数据库中拉出并放入文件中,即使它在输出时涉及适度简单的操作。存储过程,特定的导出实用程序。调查您的数据库供应商提供的其他产品。

    【讨论】:

      【解决方案3】:

      看起来您正在使用起始行号和结束行号调用 getProductIterator(),而 getProductIterator() 需要起始行和行数。随着您的“上限”越来越高,您正在以更大的块读取数据。我认为您的意思是将 batchSize 作为第二个参数传递给 getProductIterator()。

      【讨论】:

        【解决方案4】:

        不是直接的答案,但对于这种数据操作,我会使用the StatelessSession interface

        【讨论】:

          【解决方案5】:

          KeithL 是对的——你正在超越一个不断增加的限制。但无论如何,以这种方式分解它是没有意义的。滚动游标的全部意义在于您一次处理一行,因此无需将其分成块。提取大小减少了对数据库的访问,但代价是占用了更多内存。一般模式应该是:

          Query q = session.createCriteria(... no offset or limit ...);
          q.setCacheMode(CacheMode.IGNORE); // prevent query or second level caching
          q.setFetchSize(1000);  // experiment with this to optimize performance vs. memory
          ScrollableResults iterator = query.scroll(ScrollMode.FORWARD_ONLY);
          while (iterator.next()) {
            Product p = (Product)iterator.get();
            ...
            session.evict(p);  // required to keep objects from accumulating in the session
          }
          

          也就是说,错误是 getHtmlSources,因此问题可能与会话/光标/滚动问题完全无关。如果这些 html 字符串很大并且一直被引用,那么您可能只是用完了连续内存。

          顺便说一句,我没有在 ScrollableResults 上看到 getScrollableResults 方法。

          【讨论】:

          • "session.evict(p); // 上面设置缓存模式的替代方案" 这句话是错误的,缓存模式涉及 L2 和查询缓存而不是会话本身。驱逐或明确声明仍然是强制性的。
          • Gab 是正确的。我更新了答案以反映这一点。
          猜你喜欢
          • 1970-01-01
          • 2011-09-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-04-10
          • 1970-01-01
          • 1970-01-01
          • 2021-03-31
          相关资源
          最近更新 更多