Java 内存访问 Trace
注意事项
截止日期:2020 年 12 月 10 日 23:59:59 (以服务器时间为准); The deadline is firm.
提交方法:在命令行中 (请确保拥有 curl 命令) 执行 (将学号、姓名、路径替换为你的个人信息)
curl http://jyywiki.cn/upload -F course=ISER2020 -F module=PA2 -F stuid=学号 -F stuname=中文姓名 -F file=@待提交的文件路径
1. 背景
仅仅依靠程序自身的 log (printf
) 很难诊断一些运行时发生的问题,例如 GC 过于频繁等。此时,动态分析工具能在运行时收集 (或打印) 额外的信息以帮助问题的诊断。在正式进入实验之前,推荐大家了解一下 VisualVM,它是 Java 程序的动态分析框架,并且能将 JVM 的状态可视化地展示出来。
在这个实验中,我们用过程序插桩的方式 hack Java 程序,使程序在运行时,能在每次访问共享内存时,都打印一个日志。内存访问日志能用于 race detection 等动态分析。
2. 实验描述
针对一个已打包为 jar 包的 Java 程序,你希望了解程序对共享内存的读写,即为每一个共享内存访问输出一条日志信息。输出一个列表,每个条目输出一行,包含用空格分开的三部分内容:
- R/W 表示读或写
- 一个十进制表示 32 位整数,访问的线程号
- 一个十六进制表示的 64 位整数,对象标识。尽量使不同对象有不同标识,虽然你很难对于 long-running 的程序绝对做到这一点 (思考为什么?)。简单起见,可以考虑直接使用
System.identityHashCode()
。 - 对象/类访问的成员 (或数组的下标)
你可以将你的工具实现成命令行工具,用法同 java
(你会在你的命令行工具中调用 java):
$ jmtrace -jar something.jar "hello world" # jmtrace 用法用 java
R 1032 b026324c6904b2a9 cn.edu.nju.ics.Foo.someField
W 1031 e7df7cd2ca07f4f1 java.lang.Object[0]
W 1031 e7df7cd2ca07f4f2 java.lang.Object[1]
...
你可以只输出 jar 包中类的内存访问日志,忽略库函数 (java.lang
, java.util
等) 中的内存访问。
3. 实现 mtrace
不同于 Linux 的命令行工具 ltrace (通过劫持 GOT 表中的跳转地址) 和 strace (使用操作系统提供的 ptrace 机制),如果希望在更细的粒度上追踪程序的行为,通常需要对程序进行一些改写,即插桩 (program instrumentation),例如,我们可以在源代码级进行插桩:
static void foo() {
int[] a = new int [10];
for (int i = 0; i < a.length; i++) {
a[i] = 0;
}
SomeClass.staticField = 1;
someObj.otherField = someObj.field;
}
通过自动化的改写工具 (改写 AST 后再生成程序),我们可以得到 “插桩” 后的版本:
static void foo() {
int[] a = new int [10];
for (int i = 0; i < a.length; i++) {
int $t0 = 0;
a[i] = $t0;
mtrace.traceArrayWrite(a, i, $t0); // instrument added
}
int $t1 = 1;
SomeClass.staticField = $t1;
mtrace.traceStaticWrite(SomeClass, "staticField", $t1); // instrument added
SomeType $t2 = someObj.field;
mtrace.traceFieldRead(someObj, "field", $t2); // instrument add
someObj.otherField = $t2;
mtrace.traceFieldWrite(someObj, "otherField", $t2); // instrument add
}
可以看到,插桩前后程序的行为是完全一样的,但变量访问进行了一定的改写,并且在每次 field 或 array 读写之后都插入了一条 log。本质上,这是一种文本的替换,只是我们一般借助 AST 能简化这样的转换。插桩后的程序功能与原先程序等价,但可以输出额外的 trace log (在 mtrace
的方法中实现)。
Java 的源代码将会被编译成字节码 (bytecode)。Bytecode 的形式比源代码简单的多,因此对字节码进行插桩是更方便的选择。例如,字节码分析工具 ASM 能非常容易地对字节码进行改写。通过在访问内存的指令之后插入相应的记录 (访问内存的指令只有 getstatic
/putstatic
/getfield
/putfield
/*aload
/*astore
),通常是一个向 trace
方法的调用。如果考虑插桩内存访问,在字节码插桩会简单地多。
最后,我们推荐使用 JVMTI (JVM Tool Interface) 捕获 class loading 时的回调 (callback)。相当于 JVMTI 能够劫持所有 Java 的类加载,并且在加载时获得被加载类的字节码。此时就是进行插桩的最佳时机——这样甚至不会错过任何动态生成的类的插桩!
你也可以搜索互联网,查找其他可能的解决方案。例如,Java 提供了 java.lang.Instrument
,相当于是刚才过程的包装——这能省去不少事情,但也减少了 hacking 的乐趣。你可以自由选择实现的方式。
4. 提交方法与评分准则
将以下内容打成压缩包 (zip 或 tar) 上传:
- (必要) 工具的源代码和编译说明。同 PA1,请不要上传依赖、二进制文件等能够被源码生成的文件
- (必要) pdf 格式的实验报告,简述你实现 jmtrace 时主要使用的技术和遇到的挑战。实验报告不宜超过 2 页 A4 篇幅
- (可选) 工具的 binary,可能包含 jar 包、可执行文件等
本实验按照实现的正确性评分。我们会运行一些较小 (数百万次内存访问以内) 的基准程序,并检查程序的输出结果。注意并发是 Java 语言/虚拟机的一部分,这些程序可能启动多个线程。
5. Artifact
为了更好地促进后续的研究工作,将研究工作的代码以开源项目的形式发布是非常必要的。因此,在这门课程中,我们要求你选一个 programming assignment (codesim 或 jmtrace),并按照开源软件的通常标准维护。对于 artifact 来说,你要尽最大的可能为这份代码负责,因为未来其他研究者可能会基于你的代码开展其他研究工作。
特别注意:Artifact 准备是二选一的。你只需要提交一份 artifact 供 peer-review。你可以任选是 PA1 (codesim) 或是 PA2 (jmtrace) 作为 artifact。关于如何准备 Artifact 请参考 Artifact 准备指南。