JVM,JVMIT, Hotspot,OpenJDK, Dynamic Attach Mechanism, Serviceability Agent 概述

JVM,JVMIT, Hotspot,OpenJDK, Dynamic Attach Mechanism, Serviceability Agent 概述

JVM,HotSpot,OpenJDK的关系

  • JVM是一个虚拟机概念,即一个模拟真实机器的软件机器。像真正的机器一样,它有一个指令集,一个虚拟的计算机架构和一个执行模型。它能够运行用这个虚拟指令集编写的代码,就像真正的机器能够运行机器代码一样。
  • HotSpot的正式发布名称为"Java HotSpot Performance Engine",是JVM的一个实现,包含了服务器版和桌面应用程序版,现时由Oracle维护并发布。它利用JIT及自适应优化技术(自动查找性能热点并进行动态优化,这也是HotSpot名字的由来)来提高性能。
  • OpenJDK是一个开源实现HotSpot(以及JDK的许多其他部分,如编译器、api、工具等)的项目。

Dynamic Attach Mechanism 原理 简介

这是一个 Sun 扩展,允许工具“附加”到另一个运行 Java 代码的进程并在该进程中启动 JVM TI 代理或 java.lang.instrument 代理。这也允许从目标 JVM 获取系统属性。
此 API 的 Sun 实现还包括一些 HotSpot 特定方法,这些方法允许从 HotSpot 获取附加信息:

  • 本地 JVM 的 ctrl-break 输出
  • 来自远程 JVM 的 ctrl-break 输出
  • 堆的转储(dump)
  • 显示在目标 JVM 中加载的类的实例数的直方图。可以计算所有实例或仅计算“活动”实例。
  • 可管理的命令行标志的值。也可以设置此类标志。

动态附加在目标 JVM 中有一个附加侦听器线程。这是在第一个附加请求发生时启动的线程。在 Linux 和 Solaris 上,客户端创建一个名为 .attach_pid(pid) 的文件并向目标 JVM 进程发送一个 SIGQUIT。此文件的存在会导致 HotSpot 中的 SIGQUIT 的handler去启动附加侦听器线程。在 Windows 上,客户端使用 Win32 CreateRemoteThread 函数在目标进程中创建一个新线程。
然后附加侦听器线程以依赖于操作系统的方式与源 JVM 通信:

  • 在 Solaris 上,使用 Doors IPC 机制。Doors附加到文件系统中的一个文件,以便客户可以访问它。
  • 在 Linux 上,使用 Unix 域套接字。这个套接字绑定到文件系统中的一个文件,以便客户端可以访问它。
  • 在 Windows 上,创建的线程获得由客户端提供服务的管道(pipe)名称。操作的结果由目标 JVM 写入此管道。
以下翻译自 creating-your-own-debugging-tools

Dynamic Attach and Instrumentation API

Dynamic Attach机制提供了连接到运行中的VM和执行几个预定义命令之一的方法。您可以要求JVM转储Java堆、打印堆栈跟踪、更改某些VM标志、加载代理库,等等。VM自己执行命令,因此为了响应,它必须处于活动状态和正常状态。
动态附加的Java API可以在相同的tools.jar文件中找到。请注意,这是一个特定于供应商的API,只适用于OpenJDK和Oracle的JDK

连接到一个正在运行的Java进程是很简单的;您只需知道目标进程ID(pid),如下面的代码所示。(动态绑定不需要特殊的虚拟机选项。它可以连接到任何本地HotSpot JVM,除非它以-XX:+DisableAttachMechanism标志启动。)

import com.sun.tools.attach.VirtualMachine;
...
VirtualMachine vm = VirtualMachine.attach(pid);
try {
    vm.loadAgent(agentJarPath, options);
}
finally {
    vm.detach();
}

下面展示如何将Java agent注入到运行中的JVM中。Java agent是用于检测应用程序的一种程序。它应该被打包到一个JAR文件中,并包含一个具有agentmain方法的类。

Instrumentation API使Java agent能够转换现有类的字节码。与Dynamic Attach一起使用时,可以实现修改正在运行的程序的代码,即使应用程序在没有任何调试工具的情况下启动。(Elkeid就是使用Jattach,通过这样的方法实现的)

下面是一个安装新版本MyClass的简单代理:

public static void agentmain(String args, Instrumentation instr) throws Exception {

    Class oldClass = Class.forName("org.pkg.MyClass");
    Path newFile = Paths.get("/path/to/MyClass.class");
    byte[] newData = Files.readAllBytes(newFile);

    instr.redefineClasses(
            new ClassDefinition(oldClass, newData));
}

redefineClasses API的限制:类文件的新版本不能添加新方法或字段,也不能删除现有成员。基本上,只有方法体可以更改,但这对于热修复来说已经足够了。

动态附加机制需要JVM的充分合作。如果JVM挂起或too busy,那么它就没有用了。当这种情况发生时,就应该调用蛮力,例如可服务性代理(serviceability agent)。

Serviceability Agent

HotSpot Serviceability Agent (SA)从虚拟机的角度提供了Java进程的底层视角。它了解有关Java HotSpot VM内部结构的一切,包括堆布局、系统字典、编译代码、线程和堆栈。此外,这些信息可以通过清晰简单的Java API获得,因此开发人员无需使用反汇编程序和其他黑客工具就可以从中受益。

SA最初是由Java HotSpot VM工程师发明的,用于调试JDK内部的崩溃。然而,他们后来意识到它可能对更广泛的开发团队有帮助,现在它与常规的JDK捆绑在一起。我们可以通过在类路径中包含{JAVA_HOME} /lib/sa-jdi.jar开始使用SA,但是,该API不是标准的,并且可能会在将来的任何JDK版本中更改

Custom tools通常是扩展现有的Tool类,该类已经能够解析参数并附加到运行的VM上。所以我们只需要在重写的run方法中实现自定义逻辑

import sun.jvm.hotspot.runtime.VM;
import sun.jvm.hotspot.tools.Tool;

public class MyTool extends Tool {

    @Override
    public void run() {
        // Actual implementation
        VM.getVM()...
    }

    public static void main(String[] args) {
        new MyTool().execute(args);
    }
}

VM.getvm()是访问Java HotSpot VM内部结构的起点。下面的例子是使用SystemDictionary遍历所有装载的类及其类加载器。在检测与类加载相关的内存泄漏时,类似的技术可能很有用。

VM.getVM().getSystemDictionary()
        .classesDo((klass, loader) -> {
    String className = klass.getName().asString();
    System.out.print(className);

    String loaderName = (loader == null)
            ? "Bootstrap ClassLoader"
            : loader.getKlass().getName().asString();
    System.out.println(" loaded by " + loaderName);
});

以上都很简单。SA的真正功能是恢复VM结构,无论是从运行中Java进程的内存,还是在操作系统配置为创建此类转储时从异常终止的进程的核心转储恢复VM结构。SA提供类似反射的API来检查Java对象并提取所需的字段。与从同一进程中工作的反射不同,SA读取不同进程的内存或解析核心转储文件。基于这一特性的工具可以实现一些特别的技术,比如从运行中的web服务器上窃取私钥。下面的代码实现了扫描目标进程的堆,寻找java.security.PrivateKey的实例并打印它们的内容。

Klass keyClass = VM.getVM().getSystemDictionary().find("java/security/PrivateKey", null, null);

VM.getVM().getObjectHeap().iterateObjectsOfKlass(
    new DefaultHeapVisitor() {
       @Override
       public boolean doObj(Oop obj) {
          InstanceKlass c = (InstanceKlass) obj.getKlass();
          OopField f = (OopField) c.findField("key", "[B");
          TypeArray key = (TypeArray) f.getValue(obj);
          key.printOn(System.out);
          return false;
       }
    },
keyClass);

SA不需要来自目标JVM的合作,并且目前没有办法防止SA交互。 不过,这不是担心的理由。 SA通常需要root特权来附加到正在运行的进程。 另外,请记住,在附加SA时,目标JVM保持挂起状态。

JVM TI

JVM工具接口(JVM TI)是一个标准的编程接口,专门为调试、监视和概要分析打算在JVM上运行的软件而设计。 JVM TI最好的一点是它的公共规范,它不绑定任何特定的实现。 它并不要求每个JVM都提供所有的JVM TI功能; 然而,大多数流行的jvm都提供了。

该接口通过С语言头文件jvmti.h公开。 基于JVM ti的工具,称为代理(agents),通常是用C或C++编写的。 它们运行在同一个进程中,并通过调用JVM TI函数直接与JVM通信。 这个接口有点类似于Java本地接口(Java Native interface, JNI)。

代理可以在JVM启动时启动(在-agentlib或-agentpath JVM参数中指定),也可以在运行时使用Dynamic Attach机制加载。 为了支持这些选项,agent应该定义一个或多个入口点:

  • Agent_OnLoad, which is called automatically by the JVM early at startup time
  • Agent_OnAttach, which is called whenever the library is loaded at runtime

代理通常做的第一件事是获取对JVM TI环境(jvmtiEnv)的引用,这是调用JVM TI函数所必需的。

include <jvmti.h>

JNIEXPORT jint JNICALL 
Agent_OnLoad(JavaVM *vm, char *args, void *unused) {
    jvmtiEnv *jvmti;
    vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_0);

    // Initialization code

    return 0;
}

JVM TI为调试器通常做的所有事情提供了功能。 您可以管理线程、遍历线程的堆栈、迭代Java堆、查询局部变量、设置断点、操纵Java类、拦截本机方法,以及执行许多其他操作。 除此之外,代理还可以订阅事件通知:当事件发生时,JVM将调用提供的回调函数。

对JVM TI功能的访问是基于能力的; 也就是说,代理必须显式地请求它将要使用的功能。 大多数功能在运行时可用,但有些功能只能在OnLoad阶段(即没有加载类和没有执行字节码的阶段)请求。 例如,can_access_local_variables功能仅在启动时可用,因为JVM需要提前禁用某些优化,以便保留关于所有本地变量的信息。

下面的示例请求一种生成异常事件的能力,并设置回调来接收关于所有抛出的Java异常(包括已捕获的和未捕获的)的通知。

jvmtiCapabilities capabilities = {0};
capabilities.can_generate_exception_events = 1;
jvmti->AddCapabilities(&capabilities);

jvmtiEventCallbacks cb = {0};
cb.Exception = ExceptionCallback;

jvmti->SetEventCallbacks(&cb, sizeof(cb));
jvmti->SetEventNotificationMode(
JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL);

回调函数接收关于异常的所有详细信息:线程、方法和所抛出异常的字节码索引。 回调还有一个对JNI环境的引用。 这意味着您可以从内部调用任何JNI函数。 例如,您可以使用JNI调用Throwable.printStackTrace()。 因此,代理将在捕获异常之前打印所有异常,包括被忽略的异常。

void JNICALL ExceptionCallback(
        jvmtiEnv *jvmti, JNIEnv *env, jthread thread,
        jmethodID method, jlocation location,
        jobject exception, jmethodID catch_method,
        jlocation catch_location)
{   
    jclass cls = env->FindClass("java/lang/Throwable");
    jmethodID print_method = env->
        GetMethodID(cls, "printStackTrace", "()V");
    env->CallVoidMethod(exception, print_method);
}

您可以使用JVM TI做更多有用的事情。 除了异常之外,还可以跟踪类加载、垃圾收集、锁争用、线程活动等。
JVM TI经常与Java调试器代理混淆。 有一种流行的误解认为JVM TI会损害安全性并降低Java应用程序的性能。 然而,Java调试连线协议(JDWP)代理只是基于JVM ti的工具的一个例子; 技术本身并不意味着安全或性能的后果。 应用程序是否会受到代理开销的影响,完全取决于代理做什么以及它请求哪些功能。 可以将JVM TI视为JNI的一种扩展。 这项技术绝对值得一试。

参考链接

  1. creating-your-own-debugging-tools
posted @ 2021-12-28 14:50  HsinTsao  阅读(585)  评论(0编辑  收藏  举报