有时候,系统出了bug,某些变量取值不符合预期,而我们又没有打印日志, 如何能快速知道运行时这些变量的值到底是什么呢?这就需要用到dump对快照了,dump堆快照能保存dump堆的那一时刻内存中的变量,通过堆快照分析工具MAT,查找缓存的值。
下面使我们一个真实生产案例:
我们计费平台本地缓存了历史免费调用量,计费时发现计费金额有问题,通过分析,怀疑是免费调用量计算错误, 免费调用量会缓存到本地内存,因为没有打印日志,所以想知道,缓存中的值到底是什么。
我们本地缓存使用的google的guava缓存, key为一个对象类 :com.fintell.charge.vo.cache.ProductCacheKeyVo
这个对象有三个成员变量:
java.time.LocalDate searchDate // 查询日期
java.lang.String productCode // 产品编码
java.lang.String institutionCode // 机构编码
我们现在想知道缓存中机构为“hanyixiaojin”, 产品为“HYXJ_P”的历史免费调用量, 按照以下步骤进行查找。
1.dump堆快照文件
先找到java进程PID, 然后使用jdk自带的jmap命令dump堆:
jmap -dump:live,format=b,file=${文件路径} ${PID}
注意:
1.执行jmap命令的用户要与 要dump堆文件java进程的启动用户一致。
2. jamp命令的jdk版本要与启动java进程的jdk版本一致。
2. MAT导入堆快照文件并分析
此处操作省略...,不清楚MAT如何导入堆文件的,自己百度,非常简单。
3. 编写OQL语句,查找缓存key对象
OQL语句不知道是什么的同学自己百度,基本用法非常简单。
OQL类似于SQL,是jvm的对象查询语言,能够查询堆快照中存在的任意实例化的对象,分析工具MAT就是基于OQL的。
根据已知的缓存key对象的属性去查找缓存key对象,OQL语句如下:
SELECT * FROM com.fintell.charge.vo.cache.ProductCacheKeyVo pck WHERE pck.productCode.toString().equals("HYXJ_P") AND pck.institutionCode.toString().equals("hanyixiaojin")
然后在MAT中执行此OQL语句,如下图所示:
查询到4行记录,每一行代表一个ProductCacheKeyVo 类实例化的对象,说明有4个ProductCacheKeyVo类的实例化对象,点击某一行记录,即可在左侧Attributes选项卡中查看到其成员变量的名称和取值列表,如下图所示:
左侧可以看到它的三个成员变量的取值。
再点击右侧对象列表中对象左侧折叠按钮或双击对象,即可展开对象列出其成员变量,点击某个成员变量,即可在左侧Attributes选项卡中查看到其成员变量的成员变量名称和列表(这句话有点绕,好好理解一下),如下图所示:
根据业务实现逻辑,可以知道我们计费系统有两类缓存用到了这个对象,一类是产品的计费策略缓存,一类是历史免费调用量缓存,经过分析,可以判定前三个对象是属于2021年04月17日、18日和19日这三天的计费策略,最后一个对象就是我们要找的历史免费调用量的缓存对象。
重点来了,到底是如何分析的呢?这就需要进行引用关系分析了。
4. 引用关系分析
引用关系分析就是分析ProductCacheKeyVo的实例对象是被谁引用了和ProductCacheKeyVo的实例对象引用了谁,我们已经通过OQL查询到了ProductCacheKeyVo的实例对象列表,所以我们现在要找谁引用了这些实例对象作为缓存的key,从而找到缓存对象,再找到对应的缓存的值。在MTA工具中提供了with incoming references和with incoming reference,可以找到实例对象的引用关系。
右键点击第一个ProductCacheKeyVo对象,选择List objects ,如下图所示:
List objects菜单下有两个选项:
1. with outgoing reference
代表当前对象直接引用的对象的集合
2. with incoming reference
代表所有引用了当前对象的对象集合
这两个选项的功能要理解清楚,关于他们的作用,看下面的例子 :
public Class A{
private B b = new B(); // b对象
private Map map = new HashMap(); // map对象
public A(){
C c1 = new C(); // c1对象
map.put("c1",c)
}
}
Class B{
private C c= new C(); // c对象
}
Class c{
private String str="1" // str对象
}
A a = new A() // a对象
上面的示例,可以得出以下结论:
- b对象的incomming reference是对象a;
- b对象的outgoing reference是对象c;
- a的outgoing reference是对象b和map(想想为什么C1不是?);
- a的incomming reference未知,因为示例中没有给出;
- c的incomming reference对象是对象b和map;
- c的outgoing reference对象是对象str;
如果一遍没有看懂上面的示例,建议多看几遍。
再回到我们的分析中,我们要找出谁引用了ProductCacheKeyVo类的这个实例对象作为缓存的key,所以选择incomming reference菜单,如下图所示:
列表中只有一条记录,说明这个ProductCacheKeyVo的这个示例对象只被引用了一次, 展开后的对象就是引用了ProductCacheKeyVo实例对象的对象, 看图可知,是一个HashMap对象内部类的Node节点对象引用了它(不知道这里在说什么的,去看下HashMap的源码)。点击这个对象,左边Attributes选项卡列出了Node对象的四个成员变量,我们关注的是key和value,key就是ProductCacheKeyVo对象,value则是一个ProductChrargeStrategyCache对象,它是机构产品计费策略对象,很明显不是我们要找的对象。
通过这种方式我们排查前三个对象都是不同日期的计费策略缓存对象,他们都是被HashMap的node对象引用的机构产品计费策略。 再分析第四个对象的incomming reference:
5. 适用场景
因为是通过抓取堆快照的方式,所以只能保存dump堆那一时刻内存中的变量, 此种方法适用于常驻内存的变量的查找,例如缓存,全局变量等,针对那些局部变量,能否抓取到,就要看运气了。
上面示例中的堆快照文件本来想提供,但是涉及到公司的一些信息,所以大家自己写个demo,按照上面方式分析一下,就不提供了。