借鉴MAT,我也开发了MProfiler的堆分析程序原创
MProfiler要想丰富诊断项,接口是基础。这些接口在保证稳定性和性能之外,还要尽可能的丰富,这样在编写诊断项时,就能够方便的获取更多的信息做出更精确的判断了。MProfiler目前正在使用C/C++开发集成堆分析程序,我们只需要在风险诊断项中注入Service,这样就可直接调用API进行操作了,如下:
@Inject
MProfilerHeapParserService
MProfilerHeapParserService提供的接口大概可分为如下几类:
-
类,可支持的操作有 按名称查询类,查询类的所有实例,查看某个类下静态属性的具体值,某个类的父类及类加载器等信息;
-
类加载器,可支持的操作有 按名称查询类加载器,查看某个类加载器加载的所有类信息等;
-
实例,可支持的操作有 按类名称列举出相关的实例,获取某个特定实例的属性值等操作。
有了堆文件操作API后,可以很方便的完成如下的一些诊断项:
-
重复的Java对象
-
重复的Java对象数组
-
重复的基本类型数组
-
重复的字符串
-
集合的使用效率低
-
对象类型数组的使用效率低
-
基本类型数组的使用效率低
其中使用效率低是指数组或集合的容量很大,而存储的数据却非常少,重复检查是为了防止大量的重复占用了不必要的内存,关于如上诊断项的具体信息,可参考官网:http:://www.mprofiler.com
另外还有一些相对复杂的风险项也需要结合堆文件提供的信息进行判断,如判断Java日志组件是否开启了异步模式时,我们需要嵌套获取一些实例的属性值进行判断,类似于首先获取A类实例的b属性的值为v,然后在获取类型为B的v实例的c属性值等,其中用的最多的有如下两个API:
-
查询类的实例数量
-
查询某个具体实例的属性值
这两个信息可以通过实时和堆文件分析获取。
-
如果要实时获取,可通过JVMTI接口提供的IterateOverInstancesOfClass()等函数来实现,这样就可实时获取到用户实例的信息,这种操作最大的弊端就是对用户程序造成一定的干扰,尤其是某个类的实例比较多时会造成程序直接卡死,所以要使用MProfiler这类堆分析API时,要确保类的实例数量在一个可控的范围内,如100个之内。实时获取时,如果要获取实例属性值,需要先查询出类的所有实例后才能操作,因为需要指定某个特定实例的hashcode值。
-
如果通过堆文件分析获取,那么只在导出堆文件的时候会造成SWT,一旦有了堆文件后,我们就可以在MProfiler进程中分析了,这样后续的所有操作都不会对用户进程造成干扰了。缺点是对堆文件本身的分析可能会是一个不小的问题,因为有些堆文件很大,实例数量动辄就是千万以上的级别,会让堆文件分析的时间变长,这样就也会挤占一些机器资源,如CPU和内存。我们还是建议优先考虑堆文件的接口,因为是离线操作,不必太过担心多次调用或者数据量大对用户进程造成干扰等问题。
下面是我开发堆文件分析程序过程中遇到的问题及一点思考。
(1)为什么要自己开发一个堆分析程序?
我在开发之前,也调研了许多堆分析程序,这些程序由于不开源,无法轻松集成到MProfiler等原因之外,还有运行效率低,内存占用大等问题,后来我着重参考了Eclipse MAT的一些实现,这个堆分析程序的功能确实很齐全,所以我在编写自己的堆文件分析程序heapparser之前,大概阅读了MAT的实现,从中还是学到了不少。不过,我写的程序要简单的多,没有MAT处理的那么精细。另外我还参照了MAT的一些实现,生成了一些索引文件,只要我们得到了某个实例的唯一id,就可在O(1)时间内从对应的索引文件中获取到相关的信息,如获取某个数组实例的大小,获取某个实例在原堆文件中的位置,获取某个实例引用到的其它实例等,这样,当我们后续需要再次读取堆文件内容时,不需要再次分析堆文件内容,大大加快了操作堆文件的速度。这些索引文件要比原堆文件小的多,也适合更长久的保存。
(2)堆文件导出遇到的问题
堆文件导出会造成用户虚拟机SWT,这是不可避免的,而且SWT的时间可能还不短,如10秒+,这和堆文件太大以及使用的存储介质等都有关系,为了降低这种影响,MProfiler只能要求用户切走部分或全部流量。
另外还有一个问题也需要关注,就是导出的堆文件太大的问题,如果此时又遇到存储空间有限,那么堆文件的导出不会变得顺利,MProfiler为了解决这种问题,可以在堆文件导出的过程中就过滤掉一些不必要的内容,如大数组中存储的具体内容等,这些内容后期也很少会用到,不过这种操作需要Hook,这要求用户在启动时配置相关的参数才能使用。
(3)如何在堆文件分析过程中以更少的内存分析出结果?
现在的堆文件超过1G很普通,1G的文件实例大概率早就超过了一千万,算一下,如果在C++中创建一个long类型的数组存储一千万个实例的地址,需要多大的内存呢?计算得出大约为76M左右,这仅是一个非常简单的操作就需要这么多内存,如果在用户运行的目标机器上执行堆文件分析的话,其内存资源会很紧张,这是一个需要考虑的问题。
另外需要说明的是,现在Java中的一些自动扩容的集合,如HashMap,List等,如果数据量达到10万+的级别后,其操作效率会很低下,而MAT自己重新定制了一些集合,同时也尽可能的预估一些数据量,为集合指定初始大小,防止频繁的扩容。在MAT中,使用最频繁的还是Hash容器,这个容器可以加快查找效率,但会浪费掉至少30%的内存空间。
MProfiler考虑到用户目标进程的有限内存资源,采用最多的存储结构是数组,查找时使用二分查找,其O(logn)的查找速度注定要比MAT的Hash查找O(1)慢一些,不过并不存在内存浪费,也不会有扩容等操作。
(4)如何更顺利的分析比较大的堆文件?
在处理1G以上的堆文件时,要尽可能的简化为单逻辑处理,也就是一次只干一件事儿,这样才能保证每个步骤能更顺利的进行,防止了长时间运行卡死的现象。我编写的堆文件需要至少4次顺序读取堆文件,所以用的时间可能会更久一些,MAT至少也会顺序读取2次文件。每次读取要做的事情如下:
-
统计出堆文件的信息,例如计算出实例的总数,类的总数,需要保存的字符串总数(这些需要保存的字符串是类名,属性名等)等;
-
根据第1步的统计信息创建出对应大小的数组,然后将读取到的信息存储到这些数组中。在这一步中,会将所有的实例地址存储到一个数组中,后续就会以数组下标1,2,3...等来代替实例地址,这样能够以4字节的int来替换占用8字节的地址,节省一部分内存空间;
-
处理Java类,通过C++实例将Java类的相关信息存储起来,如父类,类加载器,静态字段和实例字段,子类及Java类的实例等信息;
-
处理Java对象,有一般的Java对象,Java对象类型数组和基本类型数组;
-
当需要读取某个实例的属性时,通过随机读取获取。
前4次都是顺序读取,最后一次是随机读取,只在用户调用了相关API后才会读取。将整个文件分析过程分为如上的5个处理过程,主要还是考虑到各个信息之间有依赖关系,而某些依赖信息可能在需要处理时还未读取出来。例如要保存某个类的子类时,只有等到所有的文件都读取完成后才能确定这一信息,所以我们可以在第1遍或第2遍得到类的子类信息,这样在第3遍处理Java类时,可直接将相关子类的信息保存到当前类的元信息中即可。