You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Criteria API结合Pageable查询时内存占用过高问题排查与解决方案咨询

我来帮你拆解这个问题的核心原因,以及对应的解决办法,应该能解决内存持续攀升的问题:

问题根源分析

1. SubList导致原结果集无法被GC回收

你在构建SliceImpl时用了resultList.subList(0, pageSize),但subList返回的是原列表的视图,并非独立的新集合。这就意味着包含pageSize+1个元素的原resultList会被SliceImpl间接持有引用——哪怕你detach了每个Player实体,这些实体对象仍然被列表引用,根本无法被垃圾回收。而Spring Data JPA的内置分页查询会把结果复制到全新的列表中,不会保留原结果集的引用,所以内存能正常释放。

2. Criteria API未启用只读模式

Spring Data JPA的方法查询默认会开启只读事务(@Transactional(readOnly = true)),这种模式下Hibernate不会跟踪实体的状态变化,不需要维护实体的快照等额外信息,内存开销会小很多。但你的Criteria API查询如果没设置只读模式,Hibernate会为每个实体维护状态跟踪,哪怕后续detach了实体,这些跟踪产生的额外内存也可能无法及时清理。

3. EntityManager一级缓存的隐性持有

虽然你调用了entityManager.detach(player)分离实体,但查询过程中Hibernate可能已经把一些中间对象(比如查询计划、结果集元数据)缓存到EntityManager的一级缓存里,这些对象不会随着detach操作被清理。而Spring Data JPA的方法查询在缓存管理上更高效,可能会自动清理或复用这些缓存对象。

对应的解决办法

1. 替换SubList为独立列表

把subList替换成新的ArrayList,切断对原resultList的引用,让原列表能被GC正常回收:

// 替换原来的SliceImpl构造代码
List<Player> content = hasNext ? new ArrayList<>(resultList.subList(0, pageSize)) : resultList;
return new SliceImpl<>(content, pageable, hasNext);

2. 启用只读事务和查询只读模式

在你的Repository方法上添加只读事务注解,同时给Criteria查询设置只读提示,减少Hibernate的状态跟踪开销:

// 在Repository方法上添加只读事务
@Transactional(readOnly = true)
@Override
public Slice<Player> getPlayers(int lastId, Pageable pageable) {
    // ... 原有逻辑 ...
    var query = entityManager.createQuery(criteriaQuery);
    // 设置查询为只读模式
    query.setHint("org.hibernate.readOnly", true);
    // ... 后续逻辑 ...
}

3. 优化EntityManager的清理策略

处理完每批数据后,调用entityManager.clear()彻底清空EntityManager的一级缓存,释放所有被缓存的对象(包括查询相关的元数据):

do {
    var players = playerCriteriaRepository.getPlayers(lastId, pageable);
    if(!players.isEmpty()){
        lastId = players.getContent().get(players.getContent().size() - 1).getId();
        for(var player : players){
            System.out.println(player.getFirstName());
            entityManager.detach(player);
        }
        // 处理完当前批次后清空一级缓存
        entityManager.clear();
    }
    hasNext = players.hasNext();
} while (hasNext);

4. 修正冗余的Offset设置

你的分页逻辑同时用了lastId过滤和offset,这完全是冗余的——where id > lastId已经排除了之前的所有数据,根本不需要再用setFirstResult(offset)。保留Offset可能会让Hibernate扫描更多不必要的行,甚至加载额外对象到缓存里。修改Repository中的Offset设置为固定0:

// 移除原来的offset计算,直接设置为0
query.setFirstResult(0);

验证建议

修改完成后,你可以用JProfiler或VisualVM这类工具监控内存变化,对比修改前后的内存占用情况,确认问题是否解决。也可以查看GC日志,检查是否有更多对象被正常回收。

内容的提问来源于stack exchange,提问作者Gilgalad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 08:39:09