喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

时间:2022-11-30 15:11:27

前言

点进来这篇文章的大概有两种人,一种是喜欢用Map的想看看自己是不是有可能也会踩雷,一种是不喜欢用Map的想进来看看那些喜欢用的人是怎么踩雷的。

那你要失望了,我只是单纯把公司最近代码审查时一个关于Map的小故事讲出来罢了。

如果这样用过的,可以收手了,没用过的,引以为鉴。


故事背景

我所在的是一个医疗行业互联网公司,有些地方比较严谨,比如架构和安全这块,有些地方又十分松散,就是没有好的编码规范,也没有代码审查,各自为之,*发挥,所以工作中维护项目时经常能读到一些烂代码。

久而久之也就容易形成下面这张很有名的图描述的生态:

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

所以上个月开始,公司的主管决定开始做代码审查,而且每周五开一次审查会,热烈讨论和帮助成长(公开处刑)。

而上周开会有意思的就是一段Map用法相关的代码,被特别拎出来做了讨论,我就专门把里面的片段提取出来做了简化,给大家分享一下问题以及优化方法。


错误用法

因为源代码涉及的业务内容较多,直接拎出来不便于理解,所以我专门做了简化,把核心的那块拿出来展示。

大概的业务是这样:患者去挂号,我们要查询未缴费的挂号记录集合,然后拿到里面的患者ID和挂号编码,用这两个值再去查询每个挂号下面所开的处方列表,也就是检查、药品等等之类的。

好了,下面就是被处刑的一段使用Map的简化版代码:

首先,是查询未缴费挂号记录,然后用查到的值再去查询对应的处方。

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

看着还好,问题就是第一段返回Map那里:

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

获取了挂号列表,然后进行了一次遍历,把里面自己需要的两个值专门放到Map里面再返回。

OK,其实很好理解,可能有人看了就会有疑问了,为啥不直接返回列表就好了,还要用一下Map?

根据代码本人的解释是这样的:因为他要返回的这个对象里面属性实在太多了,大概是下面这样。

(PS:param是我为了核心字段的保密而修改的,源代码里该对象共有几十个业务字段。)

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

因为他嫌弃太多,就用Map只保留了自己需要的那两个,也算解释的过去……


问题解析

1、问题到底在哪里

案例摆出来了,那么问题是什么?

就是因为Map你放在了循环里,这是大忌啊!

众所周知,生产环境查询的数据往往是动态的,比如案例中的查询挂号列表,某个时刻也许是1个,也有可能是10个,流量洪峰期更有可能百多个,你想想Map里面一次性装了多少这鬼玩意儿。

2、Map最直接作用

Map网上有非常多的文章解析和原理解析,我就不专门解释了,我这里给你说其中一个最简单直接的特点,说白了你可以把它当成项目运行时的缓存。

比如项目启动后,你可以往里面存一些静态不会变的配置信息,这样项目运行时你直接可以GET到,而不必做多余的查询,这也是在服务商模式的支付场景中很常用的一个手段,PUT多个子商户的配置信息。

3、形象地看待Map

在Java中Map是一种特殊的工具,它一旦产生就很难清理,会在内存中直接开辟一块空间,你可以想象成建了一个小仓库,当你PUT键值对的时候,其实就是往这个小仓库中放杂物。

而怪就怪在当你想要把Map清空的时候,结果只是把仓库里的杂物给丢掉了,小仓库还在。

很多初学者甚至包括已经工作几年的Java工程师都不清楚这一点,要么以为会自动释放内存,要么以为调用empty()就可以清理掉占用内存,实则不然。

明白这一点以后,你再回头想一想这个案例,一个患者查询挂号信息的时候开辟了1个小仓库,这个小仓库里面又循环存放了N个杂物,算算一个三甲医院一天估计会有多少个患者看病,每个患者都开辟1个小仓库存放一堆杂物,你觉得内存的心理阴影面积有多大。

4、真会内存泄露吗

这也是为什么我们的代码评审会专门把这位道友的片段捞出来与民同乐的原因,当你无法预估一段代码中Map生产量的时候,就是埋下内存泄露隐患的时候。

我工作至今,只有在广州工作期间遇到过一次,而且不是互联网项目,是电力行业的系统,用户量也就几万人,就因为Map使用不当导致某一天系统忽然跑不动了,一直转啊转啊转,跟着我左手右手一个慢动作……

以2014年当时的一些技术底蕴,很多中小企业并没有可靠的监控措施,纯粹靠人力巡检,所以出了这种生产环境的问题大家都傻了,然后各种分析各种讨论,好在经过大家的同心协力,最后重启一下好了

后来知道,就是内存泄露了,并且找到了核心业务中动态产生Map的代码,当时给我惊呆了,还有这种写法,还好不是我。

其实这些年换了几家公司,和不同的同事合作过,不分经验长短,写这种代码的程序员在中小企业比你想象的多,但是我遇到的出现内存泄露的就那一次,哪怕这次代码审查捞出来的也是没有在线上出现问题的代码,只是他被审查了而已。

所以,内存泄露没有你想的那么容易出现,服务器水平也和以前大不相同,动辄8核16G什么的,但不能因为这样就抱有侥幸的心态,以后没把握还是别用什么Map了,万一你走了把别人坑了呢,就当乐于助人吧。


正确用法

说下正确的用法吧,其实很简单,遇事不决List,围绕List来解决一般都没啥问题。

1、直接List

上面的案例说了,虽然字段多,但其实省事一点就直接List列表全部返回也没啥,总比你对着Map疯狂输出要强。

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

就直接返回整个List列表,别整些奇奇怪怪的,在实际工作中,安全比方便重要,亘古不变。

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙


2、拼接字符串

如果你实在不想要那么多字段,毕竟有大几十个,我就只想要我的两个小可爱。

那也简单,直接把两个小可爱做成人体蜈蚣,然后放List返回就行。

获取挂号列表的时候,直接用一个分隔符比如@,把两个你要的值拼接起来放入List返回就行。这里只是抛砖引玉,只要思路是这样方法有很多,比如你不嫌麻烦的话还可以单独创建一个DTO就传输你想要的几个字段也行。

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

然后查处方时再把两个小可爱拆开即可

喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙


总结

Map原理解析的文章多如牛毛,你可能看过就忘了,也可能看的很烦,没关系,那些都是造核弹用的,我们就拧螺丝不干别的,工作时就注意一下我总结的几点即可高枕无忧。

1、Map最直接的特点是可以作为项目运行时的缓存,多用于项目启动后存放静态数据,比如配置信息;

2、Map的产生会开辟一块内存空间,而这块内存空间的数据很好清理,但占用的内存很难清理,往往要重启后才会彻底释放;

3、在循环中使用Map是大忌,因为线上环境它的生产量难以预估;

4、查询时一旦牵扯到动态数据,千万不要使用Map,围绕List来做替代。

最后就好奇问下,您也是神仙转世吗?


原创文章纯手打,一个一个字敲出来的,如果觉得有帮助麻烦点个推荐吧~

本人致力于分享工作中的经验及趣事,喜欢的话可以进主页关注一下哦~