1. 从一次深夜告警说起:我的系统怎么“撑”死了?
那天凌晨两点,手机突然像疯了一样震动,屏幕上全是监控平台的红色告警。我睡眼惺忪地爬起来,一看就懵了:线上一个核心服务的内存使用率,在短短半小时内从40%一路飙到了98%,然后触发了OOM(Out of Memory),整个服务直接“罢工”了。这可不是小事,直接影响着几万用户的订单流程。
我赶紧连上服务器,用jmap -heap看了一眼堆内存,老年代(Old Gen)几乎被占满了。再用jmap -histo:live导出了堆里的对象直方图,好家伙,排在前面的不是什么业务数据对象,而是一堆名字里带着BoundedConcurrentHashMap和QueryPlanCache的玩意儿,占了好几个G的内存。我当时心里就“咯噔”一下:又是Hibernate的QueryPlanCache在搞鬼。
这已经不是第一次遇到了。很多用Spring Boot + JPA/Hibernate的团队,业务跑得好好的,一到流量高峰或者运行一段时间,内存就悄悄“泄漏”了,最后崩掉。表面看是JVM的堆内存不够,但根子往往藏在ORM框架的缓存机制里。这个QueryPlanCache,说白了就是Hibernate为了提升性能,把解析好的SQL执行计划给缓存起来。下次执行相同逻辑的查询时,就不用再重新解析SQL、生成执行计划了,直接拿来用,速度飞快。
听起来是个好东西对吧?但它有个致命的“坏习惯”:对于使用IN子句的查询,比如where id in (?, ?, ?),参数个数不同,Hibernate就会认为这是不同的查询,从而生成一个新的执行计划缓存起来。如果你的业务里有很多根据不同ID列表批量查询的场景,比如分页查询、批量操作,每次传入的ID数量都不同,那么缓存里就会瞬间塞满成千上万个几乎相同、只是参数数量不同的执行计划。这些缓存对象又不会被及时清理,久而久之,就像房间里堆满了不同尺寸但功能一样的工具箱,最终把整个房间(堆内存)塞爆。
我那次遇到的就是这个经典场景。一个findByIdIn(List<Long> ids)方法,被各处调用,传入的列表长度从1个到500个不等。几天下来,QueryPlanCache就膨胀成了一个吞噬内存的怪兽。重启服务后,内存使用率立刻从65%降到了20%,这就是典型的缓存泄漏特征——内存只增不减,直到耗尽。接下来,我就带你一起,像侦探破案一样,把这个问题揪出来,并分享几种我实战中验证过的、真正有效的“止血”和“调优”方案。
2. 深入虎穴:QueryPlanCache 内存泄漏的根因剖析
要解决问题,先得摸清敌人的底细。我们不能只停留在“缓存太多”这个表面,得看看Hibernate到底是怎么“攒”下这么多家当的。
2.1 QueryPlanCache 到底是何方神圣?
你可以把Hibernate执行一次查询的过程,想象成你要开车去一个陌生地方。每次出发前,你都需要(1)看懂地图(解析HQL/JPQL),(2)规划出一条具体路线(生成AST抽象语法树),(3)考虑红绿灯、单行道等限制,优化路线(生成SQL并优化执行计划)。这个过程挺耗时的。
QueryPlanCache就像一个老司机的经验库。第一次去某个地方(执行某个查询),他会把规划好的完整路线(执行计划)记在小本本(缓存)上。下次再去同一个地方,他就不用重新看地图规划了,直接掏出小本本,按记下的路线走,效率极高。
在Hibernate里,这个“执行计划”缓存主要包含两部分:
- SQL语句的执行计划:对应参数
hibernate.query.plan_cache_max_size。 - 参数元数据(Pa


467

被折叠的 条评论
为什么被折叠?



