2 个不稳定版本
0.1.0 | 2021年1月5日 |
---|---|
0.0.1 | 2020年4月3日 |
#1507 在 解析器实现 中
165KB
1.5K 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
而不是生成个体对象及其之间引用关系的图,此图生成它们之间的关系。
考虑类Foo
和Bar
,其中Foo
有一个类型为Bar
的字段b
(永不为null)。如果有172893个Foo
(和Bar
)实例,而不是在图中绘制172893个表示Foo
对象的节点,以及另外172893个表示Foo
引用的Bar
对象的节点,将有两个节点:一个用于Foo
,一个用于Bar
,它们之间有一个权重为172893的边。这样,即使是在巨大的堆转储上,也能直观地看到对象关系的模式,这在逐个检查对象时是难以处理的。
--min-edge-count
设置从给定字段到另一类型的引用数量阈值,以便将其包含在图中。较小的数字将在图中显示更多的节点,但会增加视觉杂乱。
这是使用--min-edge-count 100
在新生成的JVM的堆转储上生成的输出(单击查看全尺寸)
和--min-edge-count 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