最近公司的一个系统频繁的发生内存泄露,把server上的dump文件下载下来打开却是损坏的。用Jconsle监控server发现,线程的数量和内存的使用率都在不断上升。服务器重启后还好,运行一天之后就会发现线程数量很多,内存使用率很高,监控GC的日志发现full GC一直被调用,然后空间不能得到释放,主要是metaspace。
然后这个版本和上个版本之间唯一的区别就是几个native sql。在Jconsle的线程页面可以看到一些错误是关于其中一个修改过的方法的,这个方法以前是调用hibernate的get方法,现在改成了native sql。
后来发现在内存变高的瞬间确实有一个对这个方法的调用,查看request后看到这条数据的返回很大。这个方法被调用的时候内存一直激增,然后降低,重复出很大锯齿形,应该是GC被频繁调用,然后该方法却一直不结束。查看内存的分区发现,老年代的大小在一直升高,不能得到释放。从Thread页面还是可以看到hibernate在处理这个native sql的时候发生了一定的错误,但是不能判断是什么问题。
拿这个native sql和特点的id到数据库查询后发现,这个sql返回了4491672条数据,这显然就是问题的所在了。这个SQL使用了很大left join,一共left join了8张表,其中3张对于主表是多对多或者多对一的关系。sql 语句如下:
- SELECT *
- FROM 主表
- LEFT JOIN a
- LEFT JOIN b
- LEFT JOIN c
- LEFT JOIN d
- LEFT JOIN e
- LEFT JOIN f
- LEFT JOIN dd
- LEFT JOIN ee
- WHERE id=:id
SELECT * FROM 主表 LEFT JOIN a LEFT JOIN b LEFT JOIN c LEFT JOIN d LEFT JOIN e LEFT JOIN f LEFT JOIN dd LEFT JOIN ee WHERE id=:id
其中a,b,c都返回多条数据,a返回101条,b返回218条,c返回204条,其它表返回一条,相乘之后正好返回4491672条数据。虽然众所周知join是笛卡尔集,但是因为开发中往往用到这种many-to-one的join比较少所以容易忽略。虽然每一张表只返回一两百条数据,但是笛卡尔集就是几百万。
通过这个事情至少让我们学到一点就是不要为多对一或者多对多的关系写太多的jion,以免发生这种内存泄露问题,排查起来相当困难。另外一个启发就是不要在同一个方法里面企图返回太多的东西,API要尽量细粒度。