⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 飞天小牛肉 「小牛肉」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

上周遇到了生产环境 OOM 的问题,找了一番之后基本定位了是大文件下载导致的问题,于是在网上搜罗了一番文章,下面分享一篇优质的解决方案,整个排查思路非常清晰,小白可以直接对照着来排查。

事故发生

上周五下午运营人员反馈,笔者所负责的后台系统从 14 点以后就卡卡的,虽然页面能够正常加载,但是一直处于数据加载中,数据也提交不了,怀疑笔者的系统有BUG,当听到运营人员的反馈我的第一反应是这不可能啊,这么简单的一个后台系统,还能出事故?

处理流程

  • 摘除其中一台服务器用于保留现场,其他服务器先重启,保证系统可用。
  • 下载GC日志,系统dump文件用于分析

GC log分析

系统启动参数,JVM内存分配:-Xmx4096m -Xms4096m -Xmn2560m

观察日志可知系统每隔 40S 发生一次 Full GC,耗时 200 毫秒,回收以后系统老年代占用也不大,才 15M,但是新生代回收完还有 2 个 G。

有点不可思议,竟然不是老年代塞满了数据,而是新生代塞满了数据。

初步推测是新生代数据要晋升到老年代,结果放不进去而引起的 Full GC。

使用 MAT 对 Dump 文件进行分析

通过总图可以看出来目前系统内存占用超过 2 个 G:

点击 Histogram 进行进一步分析,看出系统中占用最多的是byte[]

点击List Objects进入income引用统计界面

层层点开,发现byte[]被 ResponseEntity 对象所引用,且数量不小

翻阅代码

1)在系统中找到唯一ResponseEntity有关的代码

2)这代码看似没什么问题啊,这不是很正常的文件下载么???我去看看用户下载了啥,跑到目录文件查看一下下。

我的天,用户下载的是一份2.4G的大文件,代码中FileUtils.readFileToByteArray(file) 的方式是把整个文件读取到内存再输出流里写入,此时内存不够分配,又塞不进老年代,只能是 Full GC 了。

3)成功破案了,用户下载了一份大文件,文件先加载到内存才往外写,抹泪。。。。

解决方案

使用FileSystemResource

@GetMapping("/down")
public ResponseEntity download(@RequestParam("uri") String uri) throws IOException {
File file = new File(uri);
if (!file.isFile()) {
throw new ServiceException("文件不存在");
}

String filename = FilenameUtils.getName(uri);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
HttpStatus status = HttpStatus.OK;
return new ResponseEntity<>(new FileSystemResource(file), headers, status);
}

使用缓存流,边读边写

@GetMapping("/down")
public void download(@RequestParam("uri") String uri, HttpServletResponse response) throws IOException {
File file = new File(uri);
if (!file.isFile()) {
throw new ServiceException("文件不存在");
}

String filename = FilenameUtils.getName(uri);
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));

try (FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(response.getOutputStream())) {
FileCopyUtils.copy(bufferedInputStream, bufferedOutputStream);
} finally {
// 使用的是try-with-resources

}
}

文件存储到 oss 或者是七牛云

将文件存储到 oss 或者是七牛云,绕过服务器下载

文章目录
  1. 1. 事故发生
  2. 2. 处理流程
  3. 3. GC log分析
  4. 4. 使用 MAT 对 Dump 文件进行分析
  5. 5. 翻阅代码
  6. 6. 解决方案