问题背景
微服务架构,当前微服务启动参数-Xmx125m。微服务各自内嵌tomcat,启动时调用tomcat 的jar包,加载业务jar包及其依赖jar包。并配置了-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$TOMCAT_LOG_DIR ,在运行过程中监控到OutOfMemoryError。
image
报错信息
首先获取日志信息:
java.lang.OutOfMemoryError: Java heap space
at org.apache.poi.hssf.usermodel.HSSFRow.createCellFromRecord(HSSFRow.java:204) ~[poi-4.0.1.jar:4.0.1]
at org.apache.poi.hssf.usermodel.HSSFSheet.setPropertiesFromSheet(HSSFSheet.java:240) ~[poi-4.0.1.jar:4.0.1]
at org.apache.poi.hssf.usermodel.HSSFSheet.(HSSFSheet.java:148) ~[poi-4.0.1.jar:4.0.1]
at org.apache.poi.hssf.usermodel.HSSFWorkbook.(HSSFWorkbook.java:356) ~[poi-4.0.1.jar:4.0.1]
at org.apache.poi.hssf.usermodel.HSSFWorkbook.(HSSFWorkbook.java:401) ~[poi-4.0.1.jar:4.0.1]
at org.apache.poi.hssf.usermodel.HSSFWorkbook.(HSSFWorkbook.java:382) ~[poi-4.0.1.jar:4.0.1]
at com.xxx.xxx.xxx.xlsio.XlsReader.read(XlsReader.java:45) ~[classes/:?]
at com.xxx.xxx.xxx.xlsio.XlsService.export(XlsService.java:18) ~[classes/:?]
at com.xxx.xxx.xxx.impl.ExportServlet.exportInternal(ExportServlet.java:162) ~[classes/:?]
at com.xxx.xxx.xxx.impl.ExportServlet.doPost(ExportServlet.java:79) ~[classes/:?]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:650) ~[servlet-api.jar:?]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:731) ~[servlet-api.jar:?]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303) ~[catalina.jar:7.0.91]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.91]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat7-websocket.jar:7.0.91]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.91]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.91]
at com.xxx.xxx.common.tokenhelper.filter.AuthFilter.authTokenByExpression(AuthFilter.java:263) ~[com.xxx.xxx.common.tokenhelper-7.303.119.jar:?]
at com.xxx.xxx.common.tokenhelper.filter.AuthFilter.uniAuthToken(AuthFilter.java:203) ~[com.xxx.xxx.common.tokenhelper-7.303.119.jar:?]
at com.xxx.xxx.common.tokenhelper.filter.AuthFilter.doFilter(AuthFilter.java:119) ~[com.xxx.xxx.common.tokenhelper-7.303.119.jar:?]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.91]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.91]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320) ~[spring-security-web-5.0.12.RELEASE.jar:5.0.12.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74) ~[spring-security-web-5.0.12.RELEASE.jar:5.0.12.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.0.12.RELEASE.jar:5.0.12.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) ~[spring-security-web-5.0.12.RELEASE.jar:5.0.12.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) ~[spring-security-web-5.0.12.RELEASE.jar:5.0.12.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347) ~[spring-web-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263) ~[spring-web-4.3.20.RELEASE.jar:4.3.20.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.91]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.91]
通过log看出,发生问题时业务在处理 excel的导入导出。调用了 apache.poi 库,实现excel文件的解析。
分析 .hprof 文件内容
接下来通过MAT查看hprof文件
image.png
从概况图上看,灰色区域还存在大量未使用内存,这块比较奇怪。
进一步,查看泄漏的详细报告。
可以看出,最大内存占用是TaskThread类,有11个实例,占用了70m的内存。
捕获.PNG
接下来,看一下dominator_tree视图,按照降序列出了内存中的对象。
TaskThread有10个左右,每个对象实例占用了接近10M内存。
TaskThread 是tomcat中的类,应该是tomcat为请求分配的线程。
image.png
展开TaskThread下面的节点。其中HSSFShee占据了绝大部分内存。
由此可以判断 是多个导出excel的请求,导致系统内存溢出。
业务流程
接下来通过业务代码分析一下,发生问题的业务流程。
此处是一个导出excel的接口。 通过请求一个id的列表,将相关数据导出到excel中,返回。
详细过程是:先读取一个excel的模板文件中,然后根据id到数据库中查询数据,然后将内容追加到读取的模板文件中,最后写出到响应报文中。
验证推测
为了排除其他原因影响,验证上述结论。在本地写了一个简单的用例。读取相同的模板文件,启动10个线程,同时调用导出excel。并也将最大内存设置为125m。
通过visual vm 观察当前的实时内存,发现和预期相同,内存上升到了100以上。并在控制台输出了OutOfMemoryError堆栈。
捕获.PNG
分析问题
通过业务日志发现,请求导出的数据量并不大,只有1两条。
而模板excel文件 大小是760kb。poi读取整个760kb文件,如果没有使用特殊的压缩算法,消耗了1m左右内存,应该也是合理的。所以并发的10此请求,会导致内存到达上限,溢出。
打开查看excel文件,发现加上表头也只有几行左右数据。但是下面有大量带颜色的空单元格,共1000行左右。从excel存储方式角度考虑,虽然内容为空,但是单元格本身是存在的,所以也会暂用空间。
解决问题
删除下方的1000多行只有颜色的空单元格。excel的大小由760kb 降到了300kb。
然后在本地用例下测试。内存降了下来。然后反复执行10次请求。内存会正常进行垃圾回收,不会导致内存溢出了。
image.png
思考
当前测试条件下,通过精简excel模板文件大小,可以很明显的降低内存。
如果业务上需要处理大尺寸的excel文件呢?
通过查询poi文档,发现poi也提供了流式处理方式,便于处理大文件。