2 个不稳定版本

0.1.0 2021年1月5日
0.0.1 2020年4月3日

#1507解析器实现

自定义许可证

165KB
1.5K SLoC

Rust 1.5K SLoC // 0.0% comments Kotlin 115 SLoC // 0.0% comments Batch 68 SLoC

包含 (JAR 文件,60KB) gradle-wrapper.jar

一个用于解析 hprof 文件格式的解析库,该格式用于 JVM 堆转储。

该库作为 crate 提供。一些基于该库构建的工具可以在 examples/ 中找到,这些工具可以作为现成的工具(特别是 ref-count-graph,请参见下文工具)或作为您自己工具的起点。

示例

如果您没有 Rust 工具链,请安装 rustup,它默认安装最新的稳定编译器。

使用 analyze_hprof 示例提供的工具之一,在本例中生成堆转储中每个类的实例计数 CSV。

cargo run --release --example analyze_hprof -- \
 -f path/to/heapdump.hprof -t 4 \
instance-counts 

生成

Instance count,Instance size (bytes),Total shallow instance size (bytes),Class name,Class obj id
114608,14,1604512,java/lang/String,34350304416
100000,24,2400000,java/util/LinkedList$Node,34350511968
2319,28,64932,java/util/HashMap$Node,34350382296
2045,0,0,[Ljava/lang/Object;,34350326864
2010,28,56280,java/util/concurrent/ConcurrentHashMap$Node,34350383896
...

如果您有非常快速的存储空间并且堆转储足够大,可以注意到差异,请尝试增加用于解析的线程数(-t)。

性能

该库解析 hprof 文件的 mmap 内容。这允许在不受系统内存限制的情况下解析巨大的堆转储,以及零拷贝解析。例如,解析 Utf8 记录类型会导致一个具有 8 字节 id 的堆分配结构体和指向映射文件内容的切片。

虽然通过使 even ids 读取自底层切片进一步推进零拷贝部分是可能的(并且很有趣),但迫切解析这些 ids 证明不是性能瓶颈,因此尚未解决这个问题。

由于更大的堆转储被分成 2GiB 的段,任何足够大以至于解析需要显著时间的堆转储都可以并行处理,即使是快速 NVMe 存储的读取吞吐量也可以被少量核心饱和。例如,instance-count 工具(请参见下文)被并行化,并在 NVMe 驱动器上以 2100-2200MiB/s 的速度解析堆转储,该驱动器在 4KiB 的随机读取时评分为 600,000 IOPS (~2300MiB/s)。使用 6 个核心时,驱动器完全饱和。

工具

通过analyze_hprof示例,可以访问许多工具子命令,其中一些将在下面详细介绍。要查看可用的子命令

cargo run --release --example analyze_hprof -- help

顶级参数,紧随所选子命令之后

  • -f - 要解析的hprof文件
  • -t - 可选;要使用的线程数(用于并行化的工具)

一些工具生成用于与Graphviz一起使用的dot文件。

子命令:build-index

一些工具需要查找每个对象ID的类ID。保留单独的磁盘索引而不是在内存中构建映射,可以处理非常大的堆转储,否则需要巨大的内存来跟踪数十亿个对象ID。

要为堆转储创建索引

cargo run --release --example analyze_hprof -- \
    -f path/to/your.hprof \
    build-index \
    -o path/to/index

子命令:ref-count-graph

而不是生成个体对象及其之间引用关系的图,此图生成它们之间的关系。

考虑类FooBar,其中Foo有一个类型为Bar的字段b(永不为null)。如果有172893个Foo(和Bar)实例,而不是在图中绘制172893个表示Foo对象的节点,以及另外172893个表示Foo引用的Bar对象的节点,将有两个节点:一个用于Foo,一个用于Bar,它们之间有一个权重为172893的边。这样,即使是在巨大的堆转储上,也能直观地看到对象关系的模式,这在逐个检查对象时是难以处理的。

--min-edge-count设置从给定字段到另一类型的引用数量阈值,以便将其包含在图中。较小的数字将在图中显示更多的节点,但会增加视觉杂乱。

这是使用--min-edge-count 100在新生成的JVM的堆转储上生成的输出(单击查看全尺寸)

ref count with min edge 100

--min-edge-count 1000,积极过滤掉不常见的边

ref count with min edge 1000

要生成图,首先根据上面所示为hprof构建索引,然后使用--index

cargo run --release --example analyze_hprof -- \
    -f path/to/your.hprof \
    ref-count-graph \
    --index path/to/index \
    --min-edge-count 50 \
    -o path/to/ref-count.dot
    
dot -Tsvg path/to/ref-count.dot -o path/to/ref-count.svg

子命令:instance-counts

输出每个类的实例计数CSV,按计数排序。

cargo run --release --example analyze_hprof -- \
    -f path/to/your.hprof \
    instance-counts

子命令:class-hierarchy

是否想以可视形式查看每个加载类的类继承层次结构?不再需要疑问。该工具生成一个图形的.dot描述,然后使用GraphViz的dot进行渲染。

cargo run --release --example analyze_hprof -- \
    -f path/to/your.hprof \
    class-hierarchy \
    -o path/to/class-hierarchy.dot
    
dot -Tsvg path/to/class-hierarchy.dot -o path/to/class-hierarchy.svg

子命令:dump-objects

当你只想查看每个对象的每个字段中的数据。

cargo run --release --example analyze_hprof -- \
    -f path/to/your.hprof \
    dump-objects

生成示例堆

sample-dump-tool子目录可以生成几种不同的对象图形状,以供您在堆分析娱乐中使用。

要查看可用的子命令

cd sample-dump-tool
./gradlew run

并且生成一个显示不同收集类型的.hprof文件,例如

./gradlew run --args collections

为什么用Rust编写JVM堆转储工具?

我与(并在)一个类似概念的库jheappo进行了一点点工作,该库是用Java编写的,这很有趣,但让我想要更多。

  • 它基于 InputStream,因此解析不能简单地并行化,并且需要多次复制所有数据
  • 每个解析记录都是堆分配的,这很方便,但并不总是理想的(垃圾收集压力,不断刷新缓存等)
  • 每个解析对象都存在Java对象开销,这意味着在对象上累积信息集合等,比其他情况下占用更多内存
  • Hprof在多个地方使用无符号数值类型,Java无法原生表示
  • 代数数据类型/求和类型在使用或编写解析器时很棒,但Java没有这些类型

Rust中的零复制解析已经在我的待办事项列表上有一段时间了,我有一个大于十亿对象的堆转储要调查,这压垮了所有我能找到的现有堆转储分析工具,所以...

依赖项

~3MB
~62K SLoC