经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
【解决方案】Java 互联网项目如何防止集合堆内存溢出(一)
来源:cnblogs  作者:CodeBlogMan  时间:2024/2/21 9:25:03  对本文有异议

前言

OOM 几乎是笔者工作中遇到的线上 bug 中最常见的,一旦平时正常的页面在线上出现页面崩溃或者服务无法调用,查看服务器日志后你很可能会看到“Caused by: java.lang.OutOfMlemoryError: Java heap space” 这样的提示,那么毫无疑问表示的是 Java 堆内存溢出了。

其中又当属集合内存溢出最为常见。你是否有过把整个数据库表查出来的全字段结果直接赋值给一个 List 对象?是否把未经过过滤处理的数据赋值给 Set 对象进行去重操作?又或者是在高并发的场景下创建大量的集合对象未释放导致 JVM 无法自动回收?

Java 堆内存溢出

我的解决方案的核心思路有两个:一是从代码入手进行优化;二是从硬件层面对机器做合理配置。


一、代码优化

下面先说从代码入手怎么解决。

1.1Stream 流自分页

  1. /**
  2. * 以下示例方法都在这个实现类里,包括类的继承和实现
  3. */
  4. @Service
  5. public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService{}

在循环里使用 Stream 流的 skip()+limit() 来实现自分页,直至取出所有数据,不满足条件时终止循环

  1. /**
  2. * 避免集合内存溢出方法(一)
  3. * @return
  4. */
  5. private List<StudyVO> getList(){
  6. ArrayList<StudyVO> resultList = new ArrayList<>();
  7. //1、数据库取出源数据,注意只拿 id 字段,不至于溢出
  8. List<String> idsList = this.list(new LambdaQueryWrapper<Study>()
  9. .select(Study::getId)).stream()
  10. .map(Study::getId)
  11. .collect(Collectors.toList());
  12. //2、初始化循环
  13. boolean loop = true;
  14. long number = 0;
  15. long perSize = 5000;
  16. while (loop){
  17. //3、skip()+limit()组合,限制每次只取固定数量的 id
  18. List<String> ids = idsList.stream()
  19. .skip(number * perSize)
  20. .limit(perSize)
  21. .collect(Collectors.toList());
  22. if (CollectionUtils.isNotEmpty(ids)){
  23. //根据第3步的 id 去拿数据库的全字段数据,这样也不至于溢出,因为一次只是 5000 条
  24. List<StudyVO> voList = this.listByIds(ids).stream()
  25. .map(e -> e.copyProperties(StudyVO.class))
  26. .collect(Collectors.toList());
  27. //addAll() 方法也比较关键,快速地批量添加元素,容量是比较大的
  28. resultList.addAll(voList);
  29. }
  30. //4、判断是否跳出循环
  31. number++;
  32. loop = ids.size() == perSize;
  33. }
  34. return resultList;
  35. }

1.2数据库分页

这里是用数据库语句查询符合条件的指定条数,循环查出所有数据,不满足条件就跳出循环

  1. /**
  2. * 避免集合内存溢出方法(二)
  3. * @param param
  4. * @return
  5. */
  6. private List<StudyVO> getList(String param){
  7. ArrayList<StudyVO> resultList = new ArrayList<>();
  8. //1、构造查询条件
  9. String id = "";
  10. //2、初始化循环
  11. boolean loop = true;
  12. int perSize = 5000;
  13. while (loop){
  14. //分页,固定每次循环都查 5000 条
  15. Page<Study> studyPage = this.page(new Page<>
  16. (NumberUtils.INTEGER_ZERO, perSize),
  17. wrapperBuilder(param, id));
  18. if (Objects.nonNull(studyPage)){
  19. List<Study> studyList = studyPage.getRecords();
  20. if (CollectionUtils.isNotEmpty(studyList)){
  21. //3、每次截取固定数量的标识,数组下标减一
  22. id = studyList.get(perSize - NumberUtils.INTEGER_ONE).getId();
  23. //4、判断是否跳出循环
  24. loop = studyList.size() == perSize;
  25. //添加进返回的 VO 集合中
  26. resultList.addAll(studyList.stream()
  27. .map(e -> e.copyProperties(StudyVO.class))
  28. .collect(Collectors.toList()));
  29. }
  30. else {
  31. loop = false;
  32. }
  33. }
  34. }
  35. return resultList;
  36. }
  37. /**
  38. * 条件构造
  39. * @param param
  40. * @param id
  41. * @return
  42. */
  43. private LambdaQueryWrapper<Study> wrapperBuilder(String param, String id){
  44. LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>();
  45. //只查部分字段,按照 id 的降序排列,形成顺序
  46. wrapper.select(Study::getUserAvatar)
  47. .eq(Study::getOpenId, param)
  48. .orderByAsc(Study::getId);
  49. if (StringUtils.isNotBlank(id)){
  50. //这步很关键,只查比该 id 值大的数据
  51. wrapper.gt(Study::getId, id);
  52. }
  53. return wrapper;
  54. }

1.3其它思考

以上从根本上还是解决不了内存里处理大量数据的问题,取出 50w 数据放内存的风险就很大了。以下是我的其它解决思路:

  • 从业务上拆解:明确什么情况下需要后端处理这么多数据?是否可以考虑在业务流程上进行拆解?或者用其它形式的页面交互代替?
  • 数据库设计:数据一般都来源于数据库,库/表设计的时候尽量将表与表之间解耦,表字段的颗粒度放细,即多表少字段,查询时只拿需要的字段;
  • 数据放在磁盘:比如放到 MQ 里存储,然后取出的时候注意按固定数量批次取,并且注意释放资源;
  • 异步批处理:如果业务对实时性要求不高的话,可以异步批量把数据添加到文件流里,再存入到 OSS 中,按需取用;
  • 定时任务处理:询问产品经理该功能或者实现是否是结果必须的?是否一定要同步处理?可以考虑在一个时间段内进行多次操作,缓解大数据量的问题;
  • 咨询大数据团队:寻求大数据部门团队的专业支持,对于处理海量数据他们是专业的,看能不能提供一些可参考的建议。

二、硬件配置

核心思路:加大服务器内存,合理分配服务器的堆内存,并设置好弹性伸缩规则,当触发告警时自动伸缩扩容,保证系统的可用性。

2.1云服务器配置

以下是阿里云 ECS 管理控制台的编辑页面,可以对 CPU 和内存进行配置。在 ECS 实例伸缩组创建完成后,即可以根据业务规模去创建一个自定义伸缩配置,在业务量大的时候会触发自动伸缩。

阿里云 ECS 管理

如果是部署在私有云服务器,需要对具体的 JVM 参数进行调优的话,可能还得请团队的资深大佬、或者运维团队的老师来帮忙处理。


三、文章小结

本篇文章主要是记录一次线上 bug 的处理思路,在之后的文章中我会分享一些关于真实项目中处理高并发、缓存的使用、异步/解耦等内容,敬请期待。

那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

原文链接:https://www.cnblogs.com/CodeBlogMan/p/18022444

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号