
已经很久没有写文章了,这期间多年的同事也被裁了,公司风雨飘摇,自己投的简历也是石沉大海,未来也是一片迷茫,感叹之际也是不能忘记学习。来补一下前面的坑,我在前面的文章 Android HPROF 内存快照文件详解 中介绍了内存快照文件的基本格式,其中的测试代码性能也不好,大文件会有 OOM
的情况,因为我当时只是针对学习 HPROF
文件格式而没有在意性能,当然那个代码也是没有办法真实使用的,还有人提到想要实操如何裁剪 HPROF
文件的大小,所以有了本篇文章。
分析 HPROF 文件的各种 Record 大小占比
我们想要裁剪 HPROF
文件,当然想要知道大致的文件中的各种内容的大小占比,这样我们动刀才有方向。我给一个我的分析结果(这只是我的这个文件的结果,千万不要生搬硬套,不同的应用有少许的不太一样,大致的方向差不多):
text
AllSize: 39.59 MB
Records:
StringRecord: Size=4.10 MB, Count=140194, SizeInPrecents=10.36 %
LoadClassRecord: Size=465.52 KB, Count=29793, SizeInPrecents=1.15 %
UnloadClassRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
StackFrameRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
StackTraceRecord: Size=12 B, Count=1, SizeInPrecents=0.00 %
UnknownRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
SubRecords:
RootUnknownSubRecord: Size=32 B, Count=8, SizeInPrecents=0.00 %
RootJniGlobalSubRecord: Size=3.80 KB, Count=486, SizeInPrecents=0.01 %
RootJniLocalSubRecord: Size=516 B, Count=43, SizeInPrecents=0.00 %
RootJavaFrameSubRecord: Size=8.46 KB, Count=722, SizeInPrecents=0.02 %
RootNativeStackSubRecord: Size=64 B, Count=8, SizeInPrecents=0.00 %
RootStickyClassSubRecord: Size=95.57 KB, Count=24465, SizeInPrecents=0.24 %
RootThreadBlockSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootMonitorUsedSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootThreadObjectSubRecord: Size=624 B, Count=52, SizeInPrecents=0.00 %
RootInternedStringSubRecord: Size=168.33 KB, Count=43093, SizeInPrecents=0.00 %
RootFinalizingSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootDebuggerSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootReferenceCleanupSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootVmInternalSubRecord: Size=973.11 KB, Count=249116, SizeInPrecents=2.40 %
RootJniMonitorSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
RootUnreachableSubRecord: Size=0 B, Count=0, SizeInPrecents=0.00 %
HeapDumpInfoSubRecord: Size=65.35 KB, Count=8365, SizeInPrecents=0.16 %
ClassDumpSubRecord: Size=9.99 MB, Count=29793, SizeInPrecents=25.24 %
InstanceDumpSubRecord: Size=7.38 MB, Count=202221, SizeInPrecents=18.64 %
PrimitiveArrayDumpSubRecord: Size=14.03 MB, Count=148756, SizeInPrecents=35.44 %
ObjectArrayDumpSubRecord: Size=2.34 MB, Count=30099, SizeInPrecents=5.92 %
ClassDumpFields:
ConstField: Size=0 B, Count=0, SizeInPrecents=0.00 %
StaticRefField: Size=2.80 MB, Count=326259, SizeInPrecents=7.07 %
StaticPrimitiveField: Size=5.70 MB, Count=637875, SizeInPrecents=14.41 %
MemberRefField: Size=185.48 KB, Count=37986, SizeInPrecents=0.46 %
MemberPrimitiveField: Size=117.30 KB, Count=24024, SizeInPrecents=0.29 %
我这里总结一下占比比较高的部分:
- 基本类型数组 35.44%
- Class 对象实例 25.24%
- 普通对象实例 18.64%
- 字符串 10.36%
- 对象应用数组 5.92%
分析的代码在这里,你也可以替换成你们自己的文件试试。
当有一个大概的分析结果了,然后我们再考虑如何裁剪。
裁剪 HPROF 文件
根据自己的不同的需求,可以有不同的裁剪方式,像我自己的需求就是想知道哪些对象占用了比较大的内存,同时想要知道这些对象到 GCRoot
的路径,而对这些对象中的内容没有那么多的关心。根据我们自己的需求就有了裁剪的方案。
裁剪基本类型的数组
占比最高的部分就是它,很多时候实际情况比我上面测试的比例可能更高。所以裁剪它的收益会很高。我对它的裁剪方案是将所有的基本类型数组中的值全部设置为 0。 为什么要设置为 0 呢?而不是直接删除,如果直接删除的话对内存大小的计算就有误了,如果设置为 0,然后再对后续的结果 zip 压缩,就能够获取到小很多的文件。我对所有的基本类型的成员变量,静态变量都是这么处理的,只保留引用类型的值,因为我们想要知道 GCRoot
的路径。
裁剪 Class 对象实例
Class
对象实例中能够裁剪的部分只有静态变量,我的方法是将非引用类型的静态变量的值全部设置为 0。
裁剪普通对象实例
普通对象实例中同样是只保留类型为引用类型的成员变量,其他类型的变量设置为 0。 这里我还要说一个小问题,当我将非引用类型的成员变量全部设置为 0 后,在 Android Profiler
中无法获取 Native
的内存占用大小,比如 Bitmap
(Android 8
以后它是占用 Native
的内存)和 Binder
所占用的 Native
内存大小。我暂时不知道是哪个成员变量保存的 Native
内存大小,如果后续知道了,可以优化一下这个部份。
裁剪字符串
在 Android
中我们需要保留类名,类中的变量名和 DumpInfoSubRecord
中的名字,就好了,其他的字符串可以全部删除; 非 Android
中的 HPROF
文件中还保留了每个线程的栈信息,每个栈帧都有对应的方法名,方法签名,还有方法源文件名字,如果你不需要也可以删除。
最后
说了半天理论也没有说服力,Show me your code. 我自己实测,原来大小为 43.86MB
的 Android
环境的 HPROF
文件,处理后大小变成了 7.05MB
(如果激进点可以压到 6MB
一下,不过会丢失一些大小数据);原来大小 419MB
的 JVM
环境的 HPROF
文件,处理后大小变成 49MB
。上面的 JVM
HPROF
文件内存占用大小为 250MB
,如果大小目前大部份虚拟机的内存大小上限 512MB
,HPROF
的文件大小最大也在 1GB
左右,处理完也就是在 100MB
左右。裁剪的测试代码。
当然你也不要那么死心眼儿,你也可以完全根据自己的需求对裁剪做出调整,比如我想要保留 Bitmap
实例中的全部信息;也可以完全不按照我的方案,但是你还是可以通过我的库 tHprofParser来实现你的裁剪方案,我是模仿 JVM
字节码修改库 ASM
来设计接口的,如果你觉得不错,希望得到你的 Star.