下图可以看出,新对象基本都是直接分配在eden区域
分析快速增加的对象
借助于java自带的工具jmap -histo pid,可以快速多次获取虚拟机堆中当前各对象的实例数量以及占用内存大小。虽然获取内存dump文件也可以,但是耗时太长,另外机器可用内存太小,dump过程可能会有副作用。
数据(jmap -histo pid):
1: 18541049 1693471216 [C
2: 1464154 679431728 [B
3: 7633015 520591584 [Ljava.lang.Object;
4: 14029600 336710400 java.lang.String
5: 293363 239603600 [I
6: 741039 231204168 com.taobao.pamirs.punish.service.domain.PunishAwardRuleDO
gc以后数据(jmap -histo:live pid):
1: 1733996 297797752 [C
2: 2884 107472896 [J
3: 206045 105961440 [B
4: 709587 72891864 [Ljava.lang.Object;
5: 66451 70673784 [I
6: 1972519 63120608 java.util.HashMap$Node
7: 2097152 50331648 com.alibaba.dts.client.executor.grid.queue.TaskEvent
8: 1730310 41527440 java.lang.String
9: 256403 31937688 [Ljava.util.HashMap$Node;
10: 430317 30525112 [Lorg.h2.value.Value;
11: 903184 21676416 java.lang.Long
12: 385777 18517296 java.util.HashMap
13: 223526 16093872 com.taobao.biz.common.division.GlobalDivisionVO
14: 225538 14434432 com.taobao.forest.store.index.bst.BSCatPropIndexNode
15: 600537 14412888 org.h2.value.ValueLong
16: 574126 13779024 java.util.ArrayList
17: 327510 10480320 java.util.concurrent.ConcurrentHashMap$Node
18: 100054 8804752 java.lang.reflect.Method
19: 320346 5125536 org.h2.value.ValueString
20: 45185 5043432 java.lang.Class
21: 200678 4816272 org.h2.value.ValueArray
22: 198356 4760544 org.h2.mvstore.db.TransactionStore$VersionedValue
23: 80812 4525472 com.taobao.hsf.remoting.service.HSFServiceURL
24: 13515 4216680 com.taobao.pamirs.punish.service.domain.PunishAwardRuleDO
上述数据排除了耗用内存较少和实例个数较少的对象,以及已知占用内存较大的对象(比如forest相关对象)。
通过对比上述数据,发现在回收以后,PunishAwardRuleDO实例个数只有28910个(将近7MB),但是在之前的数据中,竟然有将近30万个实例(将近70MB),也就是有大量的对象成为垃圾对象被回收掉了,基本可以断定PunishAwardRuleDO存在问题,接下来开始翻代码:
翻过一遍代码以后,问题就比较明显了。在punishcenter-common-1.2.3-SNAPSHOT.jar包中,存在定时刷新所有的处罚规则的缓存,3分钟一次,缓存对象FileCache至少会存活3分钟时间,如果在这三分钟里面,young gc次数超过了15次(默认值,15次以后,对象会晋升到老年代),那么这个FileCache对象就会进入老年代,那么在后续的缓存切换以后,原有的缓存对象会继续留在老年代,变成垃圾对象,随着时间积累,内存中会存在越来越多的FileCache拷贝,只能等待被fullgc回收。
结合前面的指标,younggc本身也很频繁,3分钟超过15次还是很容易的。另外,结合其他同学提供的信息,在内存dump文件中,也发现FileCache存在多份拷贝,这样更加证实了这里存在问题。
2.上面的输出中[C对象占用Heap这么多,往往跟String有关,String其内部使用final char[]数组来保存数据的.
回收一次就可以从1.69G降到200M
总结
1、排除法,缩小问题范围(比如临时下线hsf,关闭nginx等排除QPS影响);
2、分析是否存在内存泄漏(分析内存使用曲线)
3、分析老年代新增对象(优先尝试轻量级工具 jmap -histo pid)
4、根据步骤3中找到的蛛丝马迹,拉取代码,深入分析
5、与相应团队沟通确认
6、拉分支,改代码,进行验证
说明:
步骤3常用的工具就是内存dump,然后使用zprofiler或者离线工具如mat进行分析,但是进行heap dump本身会触发一次gc操作,导致dump出来的对象已经没有垃圾对象了,不利于分析,但对于内存泄漏还是有用的。此外,这个操作本身很耗时,一次dump可能需要好几分钟,不利于快速分析。
因此,可以尝试另外一个工具,jmap -histo pid 和 jmap -histo:live pid,抓取内存中各对象的统计数据(直方图),主要包含实例个数,以及占用的内存空间。其中前一个命令,是抓取所有的对象,包括垃圾对象,而后一个命令只抓取存活对象的,结合这两个命令的输出,进行比对,能够快速找出垃圾对象信息。关键是,这个命令速度很快,可以快速进行多次操作。
最后,补充一点:
遇到此类问题(包括CPU过高问题),应用代码中的定时任务(非DTS定时任务)应该是第一怀疑对象,尤其是二方包里面的,因为这种任务会在所有机器上运行,最终拉高整个集群的指标。
附 - jmap输出中class name非自定义类的说明:
BaseType Character | Type | Interpretation |
---|---|---|
B | byte | signed byte |
C | char | Unicode character |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
L; | reference | an instance of class |
S | short | signed short |
Z | boolean | true or false |
[ | reference | one array dimension |