posts

Jun 11, 2023

(续)如何从堆转储中恢复导致OOM的Excel文件

Estimated Reading Time: 2 minutes (227 words)

OOM发生后,我们可以通过堆转储文件分析出原因,那么我们能从堆转储文件里挖到更多细节吗?这一篇我们继续聊聊Excel OOM的事儿。

1 理论基础

根据MAT文档Analyzing Threads描述,hprof文件含有线程的栈以及栈里的局部变量:

Some heap dump formats (e.g. HPROF dumps from recent Java 6 VMs and IBM system dumps) contain information about the call stacks of threads, and the Java local objects per stack frame.

也就是说若程序实现是把excel解压缩之后的内容都加载到内存中的话,那么是可能提取出来的。

只要能找到内容,根据上文说到的xlxs结构,我们可以把xml等文本写到文本,再根据响应的文件结构存放,使用zip压缩后更名为xlsx文件,就算是提取了。

2 阅读源码

走一下POI的初始化workbook代码,可以发现构造器org.apache.poi.xssf.usermodel.XSSFWorkbook#XSSFWorkbook(org.apache.poi.openxml4j.opc.OPCPackage)的唯一入参的类型OPCPackage唯一实现是org.apache.poi.openxml4j.opc.ZipPackage,也许会有解压缩后的文件内容。

3 查看栈的局部变量

打开MAT的线程视图:

打开堆转储文件,根据之前的分析OOM是由线程http-nio-8080-exec-5引发的,我们导航到这个线程;点击以下按钮,展开这个线程的全部栈帧:

然后导航到方法org.apache.poi.xssf.usermodel.XSSFWorkbook#XSSFWorkbook(org.apache.poi.openxml4j.opc.OPCPackage)

关注ZipPackage类型的变量,点击它左侧的按钮展开,再递归地展开,最终能找到字段zipEntries,对应的列表既是xlsx文件内包含的所有文件:

4 提取内容到xlsx文件

找到了xml数据,我们如何构造一个xlsx文件呢,根据上文提到的xlsx文件结构:

.
├── [Content_Types].xml
├── _rels
├── docProps
│   ├── app.xml
│   └── core.xml
└── xl
    ├── _rels
    │   └── workbook.xml.rels
    ├── sharedStrings.xml
    ├── styles.xml
    ├── theme
    │   └── theme1.xml
    ├── workbook.xml
    └── worksheets
        └── sheet1.xml

看起来是比较复杂的,我们可以逆向思维:创建一个空白的xlsx,解压,然后将堆中的xml内容覆盖到对应的xml文件,再打包成zip,改拓展名为xlsx

打开Excel,新建空白的工作簿,在第一张表的某个单元格输入内容,保存到文件my-xlsx.xlsx,然后解压:

回到MAT界面,右击xl/sharedStrings.xmldata属性,Copy > Save Value To File:

覆盖到我们刚才解压的xlsx的sharedStrings.xml:

存储后确认对应文件的长度是否于堆内的长度一致:

重复上面的动作,将xl/worksheets/sheet1.xml的内容覆盖到文件。

更完善的处理应该是将堆里的10个文件都提取出来,但只是为了查看第一张表的内容,提取这里两个文件已经足够。

然后打开命令行,切换到my-xlsx文件夹,当前文件夹内容如下:

➜  my-xlsx tree .
.
├── [Content_Types].xml
├── _rels
├── docProps
│   ├── app.xml
│   └── core.xml
├── my-new.xlsx
└── xl
    ├── _rels
    │   └── workbook.xml.rels
    ├── sharedStrings.xml
    ├── styles.xml
    ├── theme
    │   └── theme1.xml
    ├── workbook.xml
    └── worksheets
        └── sheet1.xml

6 directories, 10 files

执行命令zip -r my-new.xlsx *

➜  my-xlsx zip -r my-new.xlsx *
  adding: [Content_Types].xml (deflated 71%)
  adding: _rels/ (stored 0%)
  adding: _rels/.rels (deflated 60%)
  adding: docProps/ (stored 0%)
  adding: docProps/app.xml (deflated 51%)
  adding: docProps/core.xml (deflated 50%)
  adding: xl/ (stored 0%)
  adding: xl/workbook.xml (deflated 61%)
  adding: xl/worksheets/ (stored 0%)
  adding: xl/worksheets/sheet1.xml (deflated 91%)
  adding: xl/styles.xml (deflated 60%)
  adding: xl/theme/ (stored 0%)
  adding: xl/theme/theme1.xml (deflated 80%)
  adding: xl/_rels/ (stored 0%)
  adding: xl/_rels/workbook.xml.rels (deflated 66%)
  adding: xl/sharedStrings.xml (deflated 48%)

可以发现当前文件夹内新增了文件my-new.xlsx,使用Excel打开它:

因为这个xlsx文件的构造过程不完善,所以会提示,我们选择Yes继续:

然后Excel会提示已经修复了这个文件,若选择View则可查看修复日志,选择Delete则直接查看修复之后的内容:

查看修复之后的内容:

至此提取成功。

5 总结

以上,我认识到了堆转储文件的更多功能,学习了更多排查问题的思路和方法。