起因

公司有一个系统(shit code),上线之后,不定时的会崩溃,容器没有挂掉,然后就可以查看Docker容器的日志,硕大的java.lang.OutOfMemoryError: Java heap space出现在了眼前。

准备分析

需要的工具:

  • JetBrains IDEA 或者 Eclipse
  • Java崩溃产生的转储文件(见下文)
  • 一个电脑和一个人

添加JVM参数

如果服务在容器中,则需要配置挂载点,将容器内的转储目录映射到物理磁盘,以免造成容器重启丢失。

在启动命令中的-jar前面加入-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/server.dump,然后重启服务,等待下一次崩溃。

开始分析

到了某一个时间点,终于崩溃了一次,然后到上述配置的路径中查看,发现有了一个server.dump文件。这个就是我们要分析的文件。

第一步 准备阶段

我们需要将转储文件导出到自己的电脑中的一个空目录中(没有就新建一个),然后使用IDEA以项目的身份打开这个目录。然后找到左下角的Profiler,如果没有就去View -> Tool Windows -> Profiler点一下即可。

Profiler中,右侧窗口的左下角有一个Open Snapshot...,点击这个按钮,然后打开我们下载下来的转储文件。

加载时间取决于转储文件的大小和电脑的性能

我这个转储文件足足有3GB左右的大小(

第二步 开始分析

IDEA直接就显示了直方图(Histogram)

Histogram

我的很大,你忍一下

由此我们可以看到这几个列:

列名 含义
Class 无须多言,就是类
Count 实例/对象数量
Shallow 浅堆大小,对象本身占用的空间
Retained 对象活跃时占用的空间或保留空间

我们可以看到:

  • java.lang.Stringjava.util.HashMap$Node 的实例数量过多,数百万个。
  • org.hibernate.internal.SessionFactoryImpl 的保留大小也达到了 295.05 MB。
  • org.springframework.data.jpa.mapping.JpaMetamodelMappingContext 的保留大小为 383.54 MB。

由以上信息可能得不出什么结果,我们继续追查,分析一下Biggest Objects。

Biggest Objects

如图我们可以看到已经按Retained排好序了,我们以第一项为例,因为占用的最多:

org.springframework.data.jpa.mapping.JpaMetamodelMappingContext 的保留大小到达了383.66 MB,很大,我们继续展开,可以得到:

org.springframework.data.jpa.mapping.JpaMetamodelMappingContext

persistentEntities 持久化的内容较多,看似比较正常(诶数据库设计的也是shit),继续看下一项org.hibernate.internal.SessionFactoryImpl

org.hibernate.internal.SessionFactoryImpl

我们可以看到queryPlanCache占用了过大的保留空间,缓存了大量的查询,这或许不太正常,继续向下:

queryPlanQuery

这里有很多比较大的查询,这值得深思,我找一个最大的进行分析:

嗯?735462个字符?这对吗?我们继续看看这个查询里都装了一些什么怪东西:

SQL

我嘞个 in-clause,怎么in了这么多,经过一番查找,根据 这篇文章 以及 文章中提到的另一篇文章 知道了一些线索:

在Hibernate中,带有SQL的IN子句的查询的项目中,Hibernate 会缓存这些已解析的 HQL 查询。
具体来说,Hibernate SessionFactoryImpl 包含 QueryPlanCache、queryPlanCache 和 parameterMetadataCache。
但事实证明,当 in子句的参数数量较大且变化较大时,这会成为一个问题。

初步定位问题

经过一系列的分析和查找,我们可以知道在Hibernate中,in-clause的条件会缓存下来,每次变化都会缓存,而且不能被垃圾回收,这种情况会一直持续下去,直到JVM的内存堆空间被填满,然后引发java.lang.OutOfMemoryError: Java heap space

建议采取的行动

  • HQL和SQL的查询优化,尽量避免使用in-clause。
  • 检查 Hibernate 是否有关于查询计划缓存大小或清理策略。
  • 其实,我们还可以看到一些其他问题,比如:java.lang.String占用的空间也不小,所以还需对字符串处理的相关代码进行优化。

后续待更新

附加信息 or 参考来源

  1. https://technodibble.blogspot.com/2015/02/hibernate-in-clause-outofmemory.html
  2. https://dimovelev.blogspot.com/2015/02/performance-pitfalls-hibernate-query.html
  3. 根据Hibernate 的HHH-13345 -> HHH-10782议题可得,查询计划缓存在清理之后依旧可以造成内存泄露。该议题已经修复,修复版本:5.3.5、5.4.0.RC1。
    1. https://hibernate.atlassian.net/browse/HHH-13345
    2. https://hibernate.atlassian.net/browse/HHH-10782

摸🐟从未停止,努力从未开始。