web服务挂了 java.lang.OutOfMemoryError: GC overhead limit exceeded

web服务挂了:排查问题,访问服务web端出现502,前端vue项目能正常访问,说明nginx服务没挂,后端服务可能挂了,打开日志打印,没有打印了,确定是服务挂了。
一、查看日志文件,这个接口多次出现gc

Url:/collect/import/area
Failed to complete processing of a request java.lang.OutOfMemoryError: GC overhead limit exceeded

  • 检查代码,调用频繁的接口,是否存在对象未释放,导致内存泄漏;(调用量大的是excel导入接口,下载excel数据接口)
  • 线上抓包,利用Mat内存解析工具,排查是否存在大对象;
  • 导入excel接口/collect/import/area,excel文件解析,使用了poi包workbook对象,未进行释放;
  • public TcoCollectExcelMsgDto analysisExcelFileToList(File file) {
            checkExcelFile(multipartFile);
            InputStream in = null;
            Workbook workbook = null;
            try {
                in = new FileInputStream(file);
                workbook = WorkbookFactory.create(in);
            } catch (Exception e) {
                AssertU.isTrue(false, "文件读取失败", e);
            } finally {
                Tools.release(in);
            return analysis(workbook);
    
    public TcoCollectExcelMsgDto analysisMultipartFileToList(MultipartFile multipartFile) {
            checkExcelFile(multipartFile);
            TcoCollectExcelMsgDto result = null;
            InputStream in = null;
            Workbook workbook = null;
            try {
                in = multipartFile.getInputStream();
                workbook = WorkbookFactory.create(in);
                result = analysis(workbook);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                Tools.release(workbook, in);
            return result;
    

    也可以是这种写法,只需要把要关闭资源的代码放入try语句中即可,JDK8开始已经不需要我们再手动关闭资源。

    * 解析前端传入的excel表格中数据 * @param multipartFile public TcoCollectExcelMsgDto analysisMultipartFileToList(MultipartFile multipartFile) { checkExcelFile(multipartFile); TcoCollectExcelMsgDto result = null; try (InputStream in = multipartFile.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { result = analysis(workbook); } catch (Exception e) { log.error(e.getMessage(), e); return result;
  • 下载文件,new文件流,没有trycatch,也可能会存在未进行释放风险;
  • public String realOssPathByTemplate(String fileName, String templateName, List<?> dataList){
            //创建流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            //从oss读取模板文件
            InputStream inputStream = ossClientUtil.downloadFile(templateName);
            ExcelWriter excelWriter = EasyExcel
                    .write(bos)
                    .withTemplate(inputStream)
                    .build();
            WriteSheet writeSheet = EasyExcel.writerSheet(0).build();
            excelWriter.write(dataList, writeSheet);
            excelWriter.finish();
            excelWriter.close();
            release(inputStream);
            return getOssPath(bos,fileName);
    
    public String realOssPathByTemplate(String fileName, String templateName, List<?> dataList){
            ByteArrayOutputStream bos = null;
            InputStream inputStream = null;
            ExcelWriter excelWriter = null;
            String ossPath = null;
            try {
                //创建流
                bos = new ByteArrayOutputStream();
                //从oss读取模板文件
                inputStream = ossClientUtil.downloadFile(templateName);
                excelWriter = EasyExcel
                        .write(bos)
                        .withTemplate(inputStream)
                        .build();
                WriteSheet writeSheet = EasyExcel.writerSheet(0).build();
                excelWriter.write(dataList, writeSheet).finish();
                ossPath = getOssPath(bos, fileName);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                excelWriter.close();
                release(inputStream);
            return ossPath;
    
  • 下载文件,存在个别new文件流,未进行释放;
  • @ApiOperation("错误信息导出”)
        @GetMapping("/export/errorinfo”)
        public R errorInfo(String guid) {
            if (StringUtils.isBlank(guid)) {
                AssertU.isTrue(false, "guid不能为空!”);
            //从redis中取出错误信息
            List<String> msgList = redisUtil.get(StaticValue.DECLARE_ERROR_PREFIX + ":" + guid, List.class);
            if (CollectionUtils.isEmpty(msgList)) {
                AssertU.isTrue(true, "暂无错误信息!”);
            List<DeclareTmpMsgData> msgDataList = new ArrayList<>();
            for (String msg : msgList) {
                DeclareTmpMsgData msgData = new DeclareTmpMsgData();
                msgData.setMsg(msg);
                msgDataList.add(msgData);
            String fileName = "基地档案导入报错提示_" + DateTime.getCurrentDate_YYYYMMDD() + ".xlsx”;
            //创建流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            EasyExcel.write(new BufferedOutputStream(bos), DeclareTmpMsgData.class).sheet("errorinfo").doWrite(msgDataList);
            return R.data(getOssPath(bos, fileName));
    
    @ApiOperation("错误信息导出”)
        @GetMapping("/export/errorinfo”)
        public R errorInfo(String guid) {
            if (StringUtils.isBlank(guid)) {
                AssertU.isTrue(false, "guid不能为空!”);
            //从redis中取出错误信息
            List<String> msgList = redisUtil.get(StaticValue.DECLARE_ERROR_PREFIX + ":" + guid, List.class);
            if (CollectionUtils.isEmpty(msgList)) {
                AssertU.isTrue(true, "暂无错误信息!”);
            List<DeclareTmpMsgData> msgDataList = new ArrayList<>();
            for (String msg : msgList) {
                DeclareTmpMsgData msgData = new DeclareTmpMsgData();
                msgData.setMsg(msg);
                msgDataList.add(msgData);
            String fileName = "基地档案导入报错提示_" + DateTime.getCurrentDate_YYYYMMDD() + ".xlsx”;
            //创建流
            ByteArrayOutputStream bos = null;
            BufferedOutputStream bufferedOutputStream = null;
            String ossPath = null;
            try {
                bos = new ByteArrayOutputStream();
                bufferedOutputStream = new BufferedOutputStream(bos);
                EasyExcel.write(bufferedOutputStream, DeclareTmpMsgData.class).sheet("errorinfo").doWrite(msgDataList);
                ossPath = easyExcelFileCreator.getOssPath(bos, fileName);
            } finally {
                Tools.release(bufferedOutputStream);
            return R.data(ossPath);
    
    利用内存解析工具

    图片上传有问题...

    内存分析工具,并未发现有大对象。考虑未在服务挂掉前dump文件,可能导致内存分析不是有效的。

    优化代码后,重新打包发布,服务恢复正常。
  • 对于文件流,或文件相关的初始化(代码层次,implements Closeable,实现接口Closeable),使用结束后,都要进行释放流操作,并且要严格遵守try-catch-finally。
  • 对于代码不严谨,使得项目存在内存溢出的隐患,在测试环节不容易能测试出来,发布到线上,并发量大了,就会出现服务挂掉问题,怎样能够避免此类严重风险,成为考虑的重点。
  • 我认为可以做到2点,第一,后端人员每个人的代码水平都不一样,需要定期对项目所用到的知识点讲解说明;第二,需要有经验的后端人员,对每一轮迭代的代码进行code review,排除代码风险隐患;

  • 考虑Java进程出现问题的时候可能已经被系统kill掉。因此我们需要配置JVM,让他能够在特定时间点自动生成heap dump。
    conf文件中配置
    在OOM的时候生成heap dump:
    -XX:+HeapDumpOnOutOfMemoryError
    文件生成的路径需要配置
    -XX:HeapDumpPath=/yuanben/dumps/heapdump.hprof
  • 操作linux用到的命令:
    jstat -gc pid 5000
    free -m
    jmap -dump:live,format=b,file=/yuanben/farm-web123.bin pid
    jcmd pid GC.heap_dump /yuanben/dumps/heapdump.hprof