摘要:我们找到了许多有趣的工具和组件用来检测状态的各个方面,其中一个就是在运行期通过反射了解内部机制。由于包含多种的实现,就是供具体实现比如必须继承的抽象类。调试器框架是可扩展的,这意味着可以通过继承这个抽象类来使用另一个调试器。
在日常工作中,我们都习惯直接使用或者通过框架使用反射。在没有反射相关硬编码知识的情况下,这是Java和Scala编程中使用的类库与我们的代码之间进行交互的一种主要手段。但是,使用反射仅限于JVM内部运行的Java和Scala代码。假使在运行期通过反射既能查看自己的代码又能看到JVM的代码,会有怎样的效果呢 ?
当我们开始创建 Takipi 的时候,试图寻找一种通过分析JVM堆内存来进行一些底层优化的有效方法,比如扫描一个托管堆块(managed heap block)的地址空间。我们找到了许多有趣的工具和组件用来检测JVM状态的各个方面,其中一个就是在运行期通过反射了解JVM内部机制。
译注: Takipi是一家以色列创业公司,发现了一种构建后端服务器网络的新方式,可以使用非常简化的除错流程帮助企业定位错误。
Java可服务性代理(Serviceablity Agent,简写SA)是最强大和最底层的Java调试工具之一。这个强大的工具是HotSpot JDK自带的。使用它不仅可以看到堆中的Java对象,还可以看到内部 C++ 对象,包括JVM本身。那才是真正魔法开始的地方。
反射的构成:在运行时动态检测和修改对象时,无论使用何种形式的反射都不能缺少两个必要信息。第一个是想要检测的对象引用(或者地址);第二个是对象结构描述,包括所有字段的偏移量以及它们的类型信息。如果支持动态方法调用,这个结构还需要包括类方法表 (比如 vtable)的引用,以及每个方法需要的参数。
Java反射本身是非常简单的,通过反射获取目标对象的引用与获取其它方式一样。使用 Object.getClass 通用方法(最开始从类的字节码中加载)获取字段和方法结构。真正的问题是:如何反射JVM本身?
反射的关键:令人惊奇的是, JVM通过一套公开的输出符号暴露了其内部的类型系统。 这些符号给可服务性代理(或其他工具)提供了访问JVM内部类系统结构和地址的方法。通过这些符号,几乎可以检测到JVM内部在最底层运行机制的所有方面,包括诸如原始堆地址、线程/栈地址以及编译器内部状态等。
反射实战:为了增加对这种方式的了解,启动可服务性代理的HotSpot调试程序界面,可以看到正在运行的一些功能。将 sun.jvm.hotspot.HSDB 作为主类参数启动 sa-jdi.jar,可以看到与其它JVM功能强大调试工具,例如 jmap、jinfo 和 jstack等,一样的底层信息。
具体实现:让我们仔细了解这些由JVM提供的功能。这种方法的基础是由 jvm 函数库公开导出的 gHotSpotVMStructs 结构。这个结构暴露了JVM内部的类型系统,也为我们提供了可以开始反射的根对象地址。可以通过 JNI 或者 JNA访问这个符号,调用方式与访问动态链接的操作系统公开系统库符号一样。
接下来问题就变成:怎样解析由 gHotSpotVMStructs 符号中地址的数据?从下表中可以看到,JVM不仅暴露了类型系统的地址以及根对象地址,还提供一些额外的符号用来解析需要的数据值。这些数据包括已定位的类描述符及其二进制偏移量。
清单(manifest): gHotSpotVMStructs 结构指向了类及其字段的一个列表,每个类提供了一个字段列表。对于每个字段,该结构提供了它的名称、类型以及是否静态字段。如果是一个静态字段,这个结构还会提供目标对象的访问地址。该地址可用作反射JVM内部特定组件的根,包括诸如编译器、线程处理或者收集堆系统等。
可以从这里检出 Hotspot JDK 中可服务性代理用来解析 gHotSpotVMStructs 的实际算法。
实例:既然已经大概了解了这些功能,现在看一些由该接口访问数据类型的示例。那些编写SA代理的人,在为gHotSpotVMStructs表中的类创建Java封装时克服了很多麻烦。在访问大多数内部系统时,这些封装提供一套简洁的API,不仅类型安全而且还隐藏了一些解析数据所必须的二进制工作。
为了更好地了解这些API提供的强大功能,下面是其中的一些底层类介绍:
VM 是一个单例类,它暴露了JVM的许多内部系统,比如线程系统、内存管理以及集合功能。可以把它当作JVM许多子系统的一个入口。当你研究API时,可以从这里入手。
JavaThread 可以让你从JVM角度理解一个Java线程, 同时还有(编译后的、经过解释的和本地)帧位置和类型的深入信息以及实际的本地堆栈和CPU寄存器信息。
CollectedHeap 可以让你研究收集堆(collected heap)的原始内容。由于HotSpot包含多种GC的实现,CollectedHeap 就是供具体实现比如 ParallelScavengeHeap 必须继承的抽象类。每一个都提供包含Java对象实际地址的一组内存区域。
当你在看每个类的实现时,你会发现本质上它们就是使用类似反射API来查看JVM内存硬编码的包装器。
C++中的反射:这些Java包装器中的每一个都被设计为JVM内部一个近乎完整的C++类镜像。我们知道,C++ 没有原生的反射能力,这就让我们产生了一个疑问:如何在这之间建立的纽带?
答案就是JVM开发者所了一些非常特别的事情。经过一系列C++宏指令以及大量的艰苦工作,HotSpot 开发小组手动将许多内部C++类的字段结构映射并加载到全局的 gHotSpotVMStructs 中。这个过程就是可以从外部进行反射的原因。在JVM编译期间确定了实际字段偏移量的值和布局,帮助并确保了输出结构兼容JVM目标操作系统。
跨进程连接:可服务性代理有另外一个强大的功能值得我们关注。从进程外反射一个内部运行的JVM是SA框架提供的超酷功能之一,通过把可服务性代理附加到目标JVM作为一个操作系统级别的调试器可以实现这一功能。由于 SA 代理框架是依赖于操作系统的,对于 Linux 会利用 gdb 调试器进行连接,对于Windows 则会使用winDbg (这意味着需要Windows 调试工具)。调试器框架是可扩展的,这意味着可以通过继承 DebuggerBase 这个抽象类来使用另一个调试器。
一旦建立了调试器的连接,gHotSpotVMStruct的返回地址值会传回到调试器进程,这样可以(借助操作系统)开始检测甚至是修改目标JVM的内部对象系统。这就是HSDB如何实现连接并调试一个目标JVM,包括Java代码和JVM代码。
希望本文能够勾起你的兴趣。 以我个人观点来看,这个架构是我最喜欢的JVM设计之一 。它的优雅和开放绝对令人惊叹。在我们构建 Takipi 实时编码模块时超级有用, 所以要感谢它的设计者。
你是否已经在代码中使用了这些API?非常乐意在下面的评论中听到它,或者回答你们的任何问题。
原文 Using Reflection To Look Inside The JVM at Run-time
翻译 黄飞飞
via importnew
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/69771.html
摘要:说明我们创建时传入了一个对象。在这个对象里我们调用了被代理的那个对象的方法,并且在其前后附加了方法。切面在指定的连接点被织入到目标对象中。因为采用动态代理,所以是在运行期完成织入。中的代理一个类被织入增强后,就产生一个结果代理类 showImg(/img/bVCPFw?w=853&h=426); showImg(/img/bVCPFV?w=636&h=235); 说明:图片是我其它地方...
摘要:可实现单例模式代码块初始化静态变量,只被执行一次内部类不能与外部类重名,只能访问外部类静态数据包括私有多分支选择整型或字符类型变量或整数表达式开始支持。 前言 大学期间接触 Java 的时间也不短了,不论学习还是实习,都让我发觉基础的重要性。互联网发展太快了,各种框架各种技术更新迭代的速度非常快,可能你刚好掌握了一门技术的应用,它却已经走在淘汰的边缘了。 而学习新技术总要付出一定的时间...
摘要:为了减少在中创建的字符串的数量,字符串类维护了一个字符串常量池。但是当执行了方法后,将指向字符串常量池中的那个字符串常量。由于和都是字符串常量池中的字面量的引用,所以。究其原因,是因为常量池要保存的是已确定的字面量值。 String,是Java中除了基本数据类型以外,最为重要的一个类型了。很多人会认为他比较简单。但是和String有关的面试题有很多,下面我随便找两道面试题,看看你能不能...
阅读 3228·2021-11-24 09:39
阅读 3159·2021-10-21 09:38
阅读 2397·2019-08-29 15:28
阅读 3738·2019-08-26 12:23
阅读 2617·2019-08-26 12:19
阅读 1360·2019-08-23 12:44
阅读 2127·2019-08-23 12:02
阅读 994·2019-08-22 17:05