使用 Criteria API Pageable 的内存泄漏

Posted

技术标签:

【中文标题】使用 Criteria API Pageable 的内存泄漏【英文标题】:Memory leak with Criteria API Pageable 【发布时间】:2021-08-30 10:00:39 【问题描述】:

我在 Criteria API 查询中实现了可分页功能,并且我注意到在查询执行期间内存使用量增加了。我还使用 spring-data-jpa 方法查询来返回相同的结果,但是在处理完每个批次后都会清理内存。我尝试从 EntityManager 中分离、刷新、清除对象,但内存使用量会继续上升,偶尔会下降,但不如方法查询那么多。我的问题是,如果对象被分离,什么会导致这种内存使用以及如何处理它?

Criteria API 可分页的内存使用情况:

方法查询的内存使用情况:

代码

由于我也在更新从数据库中检索到的实体,因此我使用保存最后处理实体的 ID 的方法,因此当实体得到更新时,查询不会跳过下一个选定页面。下面我提供的代码示例并非来自我正在开发的真实应用程序,但它只是重现了我遇到的问题。

存储库代码:

@Override
public Slice<Player> getPlayers(int lastId, Pageable pageable) 
    List<Predicate> predicates = new ArrayList<>();

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<Player> criteriaQuery = criteriaBuilder.createQuery(Player.class);
    Root<Player> root = criteriaQuery.from(Player.class);

    predicates.add(criteriaBuilder.greaterThan(root.get("id"), lastId));

    criteriaQuery.where(criteriaBuilder.and(predicates.toArray(Predicate[]::new)));
    criteriaQuery.orderBy(criteriaBuilder.asc(root.get("id")));

    var query = entityManager.createQuery(criteriaQuery);

    if (pageable.isPaged()) 
        int pageSize = pageable.getPageSize();
        int offset = pageable.getPageNumber() > 0 ? pageable.getPageNumber() * pageSize : 0;

        // Fetch additional element and skip it based on the pageSize to know hasNext value.
        query.setMaxResults(pageSize + 1);
        query.setFirstResult(offset);

        var resultList = query.getResultList();

        boolean hasNext = pageable.isPaged() && resultList.size() > pageSize;
        return new SliceImpl<>(hasNext ? resultList.subList(0, pageSize) : resultList, pageable, hasNext);
     else 
        return new SliceImpl<>(query.getResultList(), pageable, false);
    

遍历pageables:

@Override
public Slice<Player> getAllPlayersPageable() 
    int lastId = 0;
    boolean hasNext = false;
    Pageable pageable = PageRequest.of(0, 200);
    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);
            
        
        hasNext = players.hasNext();
     while (hasNext);
    return null;

【问题讨论】:

您能否附加一个配置文件并让它运行很长时间,以至于导致内存泄漏的原因占用了很大一部分内存?之后,配置文件应该能够指出填充该内存的对象以及阻止它们被 GC 的引用更改。这应该有助于确定根本原因。 在 52m 个实体上运行,它说主要嫌疑人是:由“jdk.internal.loader.ClassLoaders$AppClassLoader @ 0x68139d970”加载的“org.hibernate.internal.SessionFactoryImpl”实例占用 29,75 MB (60,38%)。尽管我无法将其追溯到该代码中的任何已知类。我在使用所有内存的树中注意到的最后一件事是:org.hibernate.internal.util.collections.BoundedConcurrentHashMap$Segment[32] @ 0x682175aa0 你能找到,BoundedConcurrentHashMap 中存储了什么吗? 【参考方案1】:

我认为您在这里遇到了与使用 JPA Criteria API 以及如何处理数值有关的查询计划缓存问题。 Hibernate 会将所有数值作为文字呈现到中间 HQL 查询字符串中,然后编译该字符串。可以想象,每次“滚动”到下一页都会是一个新的查询字符串,因此您会逐渐填满查询计划缓存。

一种可能的解决方案是使用像Blaze-Persistence 这样的库,它具有自定义JPA Criteria API implementation 和Spring Data integration,这将避免这些问题,同时由于更好的分页实现而提高查询的性能。

您的所有代码都将保持不变,您只需要包含集成并按照setup section 中的说明对其进行配置。

【讨论】:

以上是关于使用 Criteria API Pageable 的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

ORDER BY 使用 Criteria API

如何通过 Sort 和 Pageable 使用 Spring data JPA 开箱即用地查询数据?

Hibernate Criteria API 多选

使用方法:JPQL或Criteria API? [关闭]

使用 JPA 的 Criteria API 按日期间隔分组

使用啥:JPQL 或 Criteria API? [关闭]