通过反射跟踪JVM的运行时状态

Published: 09 Mar 2014 Category: JVM

我们经常会在工作中用到反射,要么直接使用,要么通过一些框架。在Java和Scala编程里,如果想要和我们的代码进行跟踪交互,却又希望对代码透明,最主流的一个方式就是反射。不过我们用到的反射通常都局限在Java和Scala代码里,并运行在JVM中。如果我们不仅是要跟踪自己的代码,还想跟踪JVM的代码怎么办呢?

当我们开始构建 Takipi站点的时候,我们想寻找一种能有效跟踪JVM堆内存的方式,以便进行一些偏底层的优化,比如扫描某个托管的内存块的地址空间。 Java Serviceability Agent是Java一个非常强大的底层调试工具。这个工具在HotSpot JDK中自带,通过它我们不仅能查看堆里的Java对象,还能跟踪到JVM内部的C++对象,这个才是它的真正价值所在。

反射的要素
: 当在运行时用反射来查看或者修改对象的时候,需要有两个基本的要素。第一个是需要查看的对象的引用(或者说地址)。第二个是这个对象结构的描述信息,包括字段的偏移量和它们的类型信息。如果支持动态方法调用,这个结构还包含了一个类方法表(vtable)的引用,以及每个方法的参数信息。 Java的反射是相当直接的。获取一个目标对象的引用非常简单。它的字段和方法的结构也都能通过Object.getClass()方法获取到(从类的字节码中加载的)。不过问题是你怎么对JVM进行反射呢?
开启宝库的钥匙
。幸运的是,JVM通过一些对外提供的接口将它的内部类型系统暴露了出来。通过这些接口Serviceability Agent(别的工具也是类似的)才能访问JVM内部类的结构和地址。有了它们,你就可以观察从底层观察JVM内部运行的所有细节,包括原始堆的地址空间,线程/栈的地址,内部编译器的状态。
反射的应用
。你可以启动一下Serviceability Agent的Hotspot调试器来体验一下这些功能。只需要运行一下sa-jdi.jar包中的sun.jvm.hotspot.HSDB的main方法就可以了。JVM的其它一些调试工具比如 jmap,jinfo和jstack等也是基于这些基础功能来实现的。

这是怎么实现的?我们来看下JVM是如何提供这些功能的。JVM库对外提供的gHotSpotVMStructs结构是这整个的基石。它暴露了JVM内部的类型系统以及根对象的地址,有了它们就可以进行对象的反射了。就像通过JNI或者JNA来动态链接到操作系统的一些值一样,你也可以这样来对它进行访问。 问题的关键在于你如何解析这个gHotSpotVMStructs符号里面的数据。正如下表所示,JVM不仅暴露了它的内部类型系统的地址和根对象地址,还有用以解析这些数据的一些额外的符号和值。这包含类描述信息和每个字段在这个类里的偏移量。

描述信息
。gHotSpotVMStructs结构指向了很多类以及它们的字段。每个类都有一系列的字段,每个字段又都包含它们的名字,类型,以及是否是静态的。如果是静态字段这个结构还可以用来访问它的值。对于一个静态的对象字段,这个结构体还会提供目标对象的地址。通过这个根地址我们可以开始反查JVM内部的一些组件,包括编译器,线程还有堆。 你可以下载HotSpot JDK的源码来了解下Serviceability agent的具体的算法。

开始动手吧
。我们已经大概了解了这些功能到底能干什么,现在来看使用这个接口获取信息的一些具体例子。SA项目的这些人费了好多工夫来给gHotSpotVMStructs暴露出的这些类来生成Java的包装类。通过这些包装类提供出来的接口让访问JVM内部系统的工作变成非常 简单和方便,不仅保证了类型安全,同时也解决了访问和解析数据的烦恼。 为了让你能理解这个接口提供的强大的功能,下面列出了它提供的一些底层类的相关信息—— VM, 它是一个单实例对象,它暴露了JVM内部的许多子系统,比如线程子系统,内存管理和回收子系统。它是JVM各个子系统的入口,想要开始探索这个API的话,这是个不错的地方。 JavaThread,通过它你可以了解到JVM内部是如何处理线程的,详细到各个栈帧的位置和类型(编译的,解释执行,或者本地的),甚至本地栈和CPU寄存器的信息都有。 CollectedHeap,通过它你可以看到堆的原始内容。HotSpot提供了多个垃圾回收的实现,这是那些具体实现比如 ParallelScavengeHeap的抽象基类。每个堆都是一些内存块的集合,这些内存块包含Java对象的确切地址。 你如果看一下这些类的实现你会发现它们实际上就是使用类似反射的API来查看JVM的内存的一些硬编码的包装类。

C++的反射
。每个包装类其实就是JVM内部的C++类的一个镜像。我们都知道C++原生是不支持反射的,那么这个桥接的工作是如何完成的呢? 这是由于JVM开发人员做了一个独特的事情。通过一系列的C++宏和许多细致的工作,HotSpot团队手动将JVM内部的C++类的字段映射并加载到了全局的gHotSpotVMStructs结构里。这才使得这些对象能从外部进行反射。真实的字段偏移量和内存布局是在JVM编译的时候生成的,这保证了导出的结构是和JVM的目标操作系统兼容的。
进程外连接
。这又是一个值得一提的SA的重量级选手。SA框架中的一个很赞的功能就是它能从进程外部来查看一个活动的JVM。这是通过将SA作为一个操作系统级的调试器连接到目标的JVM来完成 的。具体的实现取决于不同的操作系统,在Linux上的话SA框架会创建一个gdb调试器的连接,在Windows上它用的则是winDbg(这个还会用到Windows Debugging Tools)。调试器框架是可扩展的,如果你想使用别的调试器的话就继承下DebuggerBase就好了。 一旦调试器连接建立起来了,返回的gHotSpotVMStruct地址就会传到调试器进程里面,它就可以查看甚至修改目标的JVM的内部对象了。这就是HSDB连接并调试目标JVM的方法,Java和JVM的代码都能进行调试。 希望这能激起你的兴趣。我个人看来,这个架构是JVM里我喜欢的特性之一。它的优雅和开放令我惊讶不已。我们在构建Takipi的一些实时编码的模块时,它也起了很大的作用,在这里我要对它的设计者们脱帽致敬。

原创文章转载请注明出处:通过反射跟踪JVM的运行时状态

英文原文链接