pdfbox java.lang.outofmemoryerror_Apache PdfBox
业务叙述
技术是为了业务服务的,所以讨论技术的时候离不开业务场景。故此先进行简述:
最近在做一个从数据库读取pdf文件并且解析成文字进行分析的功能。由于本人对垃圾回收也是略有涉猎,因此代码设计上基本不存在本应该回收但是却无法回收的情况。然而,这个程序用springboot启动后一段时间就会出现oom错误,错误日志如下:
java.lang.OutOfMemoryError: Java heap space
at oracle.sql.BLOB.getBytes(BLOB.java:217)
at org.apache.ibatis.type.BlobTypeHandler.getNullableResult(BlobTypeHandler.java:43)
at org.apache.ibatis.type.BlobTypeHandler.getNullableResult(BlobTypeHandler.java:28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:521)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:402)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:67)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at sun.reflect.GeneratedMethodAccessor34.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426)
at com.sun.proxy.$Proxy60.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:158)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:76)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:61)
at com.sun.proxy.$Proxy65.findAttachments(Unknown Source)
at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
从日志中可以看出,在获取数据库中的Blob数据时,出现了Java heap space不足,也就是堆满了。在此科普一下几种OOM的情况:
PermGen space
发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Permanent Generation space有关。常用的解决方法是调整XX:PermSize和XX:MaxPermSize参数的大小。
unable to create new native thread
顾名思义就是不能创建native的线程。遇到这种情况还是比较复杂的,具体遇到了再分析。
Java heap space
发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。这是比较常见的情况。
分析错误
jvisualvm
jvisualvm是一个性能分析工具,自从 JDK 6 Update 7 以后已经作为 Oracle JDK 的一部分,位于 JDK 根目录的 bin 文件夹下。jvisualvm自身要在 JDK6 以上的版本上运行,但是它能够监控 JDK1.4 以上版本的应用程序。
虽然jdk9之后这个工具被移除了,但是恰好当前项目是java 8的,jvisualvm依然可以使用。
具体用法:在jdk的bin目录下启动,或者如果环境变量已经配好,直接在控制台下输入jvisualvm也可以启动
当然我这里不是写怎么用这个工具,具体操作不细述。有了工具就可以开工了。以下是整个分析的过程。
怀疑1:byte数组占用内存,未及时清理
这个怀疑很简单,因此日志报的是在读取blob格式数据时内存溢出。因此我第一反应就是在当字节数组使用完后进行一次gc。
启动应用后java堆如下图:
图1
图上可以明显看出来成过山车式的效果,运行一段时间后再次出现OOM。利用jvisualvm的dump功能进行分析,在山顶和山崖下分别dump堆信息进行比对(忘记截图了)发现大字节数组差异不明显,显然问题不是字节数组造成的。
怀疑2:奇技淫巧stream会不会有问题
由于喜欢用stream写程序,恰好分析这段代码又是在forEach块中,因此怀疑是不是这个写法有坑。于是改成普通的循环,再次查看堆:
普通的for循环
从统计学上看,这次的结果和上次没有很大区别,出现了一段400M维持的情况。
怀疑3:正则解析产生大量临时对象
比对heap图中波峰段和日志情况,基本上每个波峰段都是在解析文件时出现。看过老夫github page的小伙伴应该知道,老夫号称正则小王子,所以解析文字必然使用正则。到这时候我是有点担心的,虽然我理解中正则引擎是计算密集型,但是毕竟没读过源码,万一恰好是坑咋办?但是再细读代码和分析dump文件,我感觉事情没那么简单(当然也有不敢相信就是这么一回事)因为所有的pattern对象我都是预编译好的,在解析的时候了不起生成一些matcher对象,再新建一些String或者char数组。如果是这样,那么波峰应该是一段明显的斜线上升,而从图上又可以看出在解析的时候波峰呈平缓上升。故此我怀疑是在解析文件之前读取环节出的问题。
怀疑4:pdfBox解析pdf时产生大量冗余
由于文件读取速度非常快,而jvisualvm的时间轴是秒为单位,在以毫秒为单位的程序中很难看出端倪,因此在文件读取后添加Thread.sleep(2000)方法。
结论
最终通过比对日志时间和heap时间轴发现是pdfBox解析文件时产生大量临时对象(由于pdfBox封装的很厉害,整个解析代码非常少,因此不会是我人为的操作导致的),因此把gc标志放到文件读取完成后。再次查看heap,效果如下图:
image.png
至此问题算是解决了。由于是上班时间解决的,因此没有截很多图,中间略去通过日志和heap图形化界面对比找到罪魁祸首的步骤。看不懂的小伙伴多思考一下了哈~
整体来说这是本人第一次遇到oom并解决的过程,还是蛮爽的,毕竟很多时候我们学习时都是只闻其声不见其庐山真面目。但是真的遇到了也不需要惊慌,但凡别人可以,自己不可以,那都是细节小问题;如果别人不可以,自己可以,那是别人没拷贝全(想到今天组里小姑娘CV老夫代码时的少初始化了一个变量,折腾一下午);别人不行,自己也不行,折腾折腾,也许就行了。
更多推荐

所有评论(0)