Bug 年年有,今年特别多
大家好,我是鱼皮。
今年真不错,红红火火,一写代码就一片红,Bug 特别多!
红红火火
这还没过多久,我就遇到了几个大 Bug。前段时间我刚上线的面试刷题网站(mianshiya.com)的 Bug 就不说了,这网站基本已经变成大家的靶场了。
今天给大家分享一个我在工作中遇到的 Bug,给我整得一愣一愣,给大家乐呵乐呵。
Bug 大赏之系统崩了
我在工作中负责的是 BI(商业智能)系统,大家可以简单理解为一个数据分析平台。
虽然平时 Bug 不多,但其实是因为用户太少了,很多 Bug 只是还没被发现而已。
这不,虽迟但到。那是一个风和日丽的上午,老板突然找到我说:整个系统崩了,用户看不到内容,你快查查!
然后我看了下日志,原来是 Java 后台程序发生了 OOM(Out Of Memory 内存溢出)!这可是个 Java 的经典老 Bug 了,而且面试官很喜欢问。
不过我和这位 Bug 已经很久没见,一时间竟有些陌生,也忘了从哪儿开始排查了,更不用说那些 JVM 常用参数和命令了。
不管怎样,我先把部分容器(程序运行的环境)进行重启,然后留一台用于排查分析。
先看日志,能看到一些 OOM 相关的报错,以及大概是哪个线程、哪段代码导致了 OOM:
查看日志
但我特么对着日志提示的代码看了半天,也没发现哪里写的有问题啊!我一个 “专业” 的程序员,难道还看不出自己的代码有问题么?
好吧,既然日志看不出问题,那就老老实实走套路。通常遇到 OOM 内存溢出时,我们要 dump 堆内存进行分析。这 dump 内存可真是一件美事,由于内存占用很大,所以等了 40 多分钟才得到了 dump 文件。
等待时顺便祈祷一下
为了便于对比分析,我还 dump 了两次不同状态的内存,每个文件 2 - 3 个 G!
dump 内存文件
然后可以用 JVisual VM 或者 Memory Analyzer Tool(MAT)之类的工具打开 dump 文件,看看到底是哪些对象占用了内存、有没有大对象没被回收掉导致内存慢慢占满等等。
仔细一看,基本内存都是被 List 列表对象占用了,列表项足足有一百万条,直接占了 1.6 GB!
MAT 内存分析工具
但是这些对象都是 GC root Unreachable(没被引用)可回收的呀,一般用完就清理了,怎么会把内存撑爆呢?
我又用去线上容器中输入查看 GC 状态的命令进行分析,发现的确触发过几次 Full GC,回收过对象呀!
那到底为啥会 OOM 呢?
真相只有一个,因为同时处理的数据量太大,导致直接把内存挤爆了!
就像我们去丢垃圾,大家慢慢丢,然后等工人来回收垃圾、清空垃圾箱,之后我们再接着丢。虽然要等待回收,但起码不会爆。
但假如有个大垃圾想自己跳进垃圾箱里,结果垃圾箱装不下,那这个大垃圾就无可奈何了。
唉,这锅肯定是我来背了。最初申请容器资源时,没考虑到竟然会有这么大量的数据,只申请了 8 G 的内存空间。
不过按道理来说 1.6 G 不到 8 G 的 1 / 4,内存应该也不会爆?
别忘了,JVM 最大堆内存通常默认是系统内存的 1 / 4,正好是 2 G。再加上 JVM 分代回收,新生代一般只占堆内存的 1 / 3,老年代则是 2 / 3,如下图:
图片来源于网络
所以当一个超过老年代大小的对象要加载到内存时,新生代装不下,直接塞到老年代。发现老年代也装不下,会先触发 Full GC(垃圾呼吸 - 十一之型 - 全回收)。而如果 Full GC 后还是装不下这个对象,就 OOM 了,凉凉!
所以最简单粗暴的方式就是调整 JVM 最大堆内存参数,比如 -Xmx8192m,从而增大堆内存空间,装下大对象。
可以用 jmap 命令来查看 JVM 堆的参数,如下图,是我之前在 JDK 11 版本截取的一个示例:
JDK 9 的常用命令有变,新增了 jhsdb,要用到时上网查就好。
查看 JVM 参数
但这种方式只能解决燃眉之急,归根结底还是要看看到底是什么数据一次性占了 1.6 个 G!
仔细一看,List 里面装的都是 Map,每个 Map 表示数据库中的一行数据,总共 100 万行 15 列。
这,这不对吧,这些数据在数据库、Excel 文件里才不到 100 MB,咋变成 Map 直接膨胀了十几倍?
于是,我试着输出了一下 List<Map> 对象的大小,发现 105 行 * 10 列就占用了 155.9 KB!100万行的话真就 1.6 个 G 了!
那一刻,我突然想起了被八股文支配的恐惧,Java 的 HashMap 占用空间的确是很大的!每个 HashMap 对象包含 12 字节的 Java 对象头、4 个引用字段、3 个 int 型字段、1 个 float 字段,这些加起来是 44 字节。因为 JVM 要求对象内存大小必须是 8 字节的倍数,所以还要再补上 4 个字节,最终一个空的 HashMap 就占用了 48 字节。用它来存数据,可比其他格式的文件要大多了!
也是由于经验不足 太过自信了吧,我事先真没想到会有这么大的数据量,并且估错了数据对象占用的空间。
不过吃一堑长一智,下次再遇到类似问题(希望不会有下次),我应该就能很快解决啦。实践 翻车,印象真的太深刻了!所以建议大家在背八股文的同时,还是多多写代码做实验哦~
至于这个问题怎么解决。。那就别用 HashMap 呗!用 List 下标来表示一行数据应该是可行的。当然,大家有更好的方案欢迎讨论~