基于SpringBoot和阿里的OSS实现了一个下载文件的功能。
大概原理是这样的:
用户请求下载资源,服务端接收到请求之后从OSS中将用户需要的资源捞出来,然后以流的方式写给客户端。
遇到一个这样的问题:
下载小文件没有问题,在超过100M左右时会报错org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
;
猜想
1、怀疑可能是资源没有关闭造成(不是)
2、堆配置太小(治标不治本)
3、代码问题(原因)
排查过程:
首先观察发送请求,请求下载一个大文件(2G);此时使用JavaVisualVM观察内存情况,会直观的看到有个byte数组的大小直线上升;
请求前:
可以看到byte[]仅占内存2%左右;
请求后:
可以看到byte[]数组内存占用达到了90%左右;
确定是这个数组的问题,检查代码,可以看到在代码中的做法是先将请求的文件从OSS中拿到,然后赋值给一个InputStream流
...
// 从oss对象上获取文件内容
InputStream is = ossObject.getObjectContent();
...
然后将is流中的内容再写到OutputStream实现类的输出流中
...
os.write(buffer, 0, bytesRead);
...
最后将输出流中的内容通过OutputStream实现类的toByteArray()方法转换成字节数组;然后返回给客户端
...
byte[] fileOut = os.toByteArray();
return fileOut;
...
经过检查确定是byte数组的问题,当OSS中文件太大时byte数组的大小会持续顶内存,直到GC处理不过来,抛出 Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
异常。
解决
去掉中间变量,直接从Response中获取OutputStream流对象;从OSS中获取到文件之后直接通过从Response中获取OutputStream流对象写给客户端;这样我们的服务端只相当于提供了一个通道功能,我们服务端只负责通道的对接,而不用做文件的中转站,大大节省服务器资源;也解决了标题中的问题。