JVMTM Tool Interface

Version 21.0


JVM工具接口是什么?

JVM工具接口(JVM TI)是开发和监控工具使用的编程接口。它提供了一种检查状态和控制在Java虚拟机(VM)中运行的应用程序的方式。
JVM TI旨在为需要访问VM状态的各种工具提供VM接口,包括但不限于:性能分析、调试、监控、线程分析和覆盖分析工具。
JVM TI可能并非所有Java虚拟机实现中都可用。
JVM TI是一个双向接口。JVM TI的客户端,以下简称为代理,可以通过事件被通知有趣的发生。JVM TI可以通过许多函数查询和控制应用程序,无论是响应事件还是独立于事件。
代理在与执行被检查应用程序的虚拟机相同的进程中运行并直接与之通信。这种通信是通过本机接口(JVM TI)进行的。本机进程接口允许在最大程度上控制工具而对目标应用程序的正常执行几乎没有干扰。通常,代理相对较小。它们可以由实现大部分工具功能的单独进程控制,而不会干扰目标应用程序的正常执行。

架构

工具可以直接编写到JVM TI,也可以通过更高级别的接口间接编写。Java平台调试器架构包括JVM TI,但还包含更高级别的、独立于进程的调试器接口。对于许多工具来说,更高级别的接口比JVM TI更合适。有关Java平台调试器架构的更多信息,请参阅Java平台调试器架构网站

编写代理

代理可以用支持C语言调用约定和C或C++定义的任何本机语言编写。
用于使用JVM TI的函数、事件、数据类型和常量定义在包含文件jvmti.h中。要使用这些定义,将J2SE包含目录添加到您的包含路径中,并在源代码中添加
#include <jvmti.h>
    

部署代理

代理以特定于平台的方式部署,但通常是动态库的平台等效物。例如,在Windows操作系统上,代理库是“动态链接库”(DLL)。在Linux操作环境中,代理库是一个共享对象(.so文件)。
可以通过指定代理库名称的命令行选项在VM启动时启动代理。一些实现可能支持一种机制来在活动阶段启动代理。如何启动这一点是特定于实现的。

静态链接代理(自版本1.2.3起)

本机JVMTI代理可以与VM静态链接。库和VM映像的组合方式取决于实现。如果代理L的映像已与VM组合,则仅当代理导出一个名为Agent_OnLoad_L的函数时,代理L被定义为静态链接
如果一个静态链接代理L导出一个名为Agent_OnLoad_L的函数和一个名为Agent_OnLoad的函数,则将忽略Agent_OnLoad函数。如果代理L是静态链接的,则将调用Agent_OnLoad_L函数,参数和预期返回值与Agent_OnLoad函数指定的相同。一个被静态链接的代理L将禁止以相同名称的代理动态加载。
如果一个静态链接的代理L导出一个名为Agent_OnUnload_L的函数和一个名为Agent_OnUnload的函数,则将忽略Agent_OnUnload函数。如果一个静态链接的代理L导出一个名为Agent_OnUnload_L的函数和一个名为Agent_OnUnload的函数,则将忽略Agent_OnUnload函数。
如果一个静态链接的代理L导出一个名为Agent_OnAttach_L的函数和一个名为Agent_OnAttach的函数,则将忽略Agent_OnAttach函数。如果一个静态链接的代理L导出一个名为Agent_OnAttach_L的函数和一个名为Agent_OnAttach的函数,则将忽略Agent_OnAttach函数。

代理命令行选项

下面使用术语“命令行选项”指的是在JNI调用API的JNI_CreateJavaVM函数的JavaVMInitArgs参数中提供的选项。
在VM启动时,以下两个命令行选项之一用于正确加载和运行代理。这些参数标识包含代理的库以及在启动时传递的选项字符串。
-agentlib:<agent-lib-name>=<options>
-agentlib:后面的名称是要加载的库的名称。库的查找,包括其完整名称和位置,按照特定于平台的方式进行。通常,<agent-lib-name>会扩展为操作系统特定的文件名。选项<options>将在启动时传递给代理。例如,如果指定选项-agentlib:foo=opt1,opt2,VM将尝试从Windows下的系统PATH中加载共享库foo.dll,或者从Linux下的LD_LIBRARY_PATH中加载libfoo.so。如果代理库已静态链接到可执行文件中,则不会进行实际加载。
-agentpath:<path-to-agent>=<options>
-agentpath:后面的路径是要加载库的绝对路径。不会发生库名称扩展。选项<options>将在启动时传递给代理。例如,如果指定选项-agentpath:c:\myLibs\foo.dll=opt1,opt2,VM将尝试加载共享库c:\myLibs\foo.dll。如果代理库已静态链接到可执行文件中,则不会进行实际加载。
对于动态共享库代理,将调用库中的启动例程Agent_OnLoad。如果代理库已静态链接到可执行文件中,则系统将尝试调用Agent_OnLoad_<agent-lib-name>入口点,其中<agent-lib-name>是代理的基本名称。在上面的示例-agentpath:c:\myLibs\foo.dll=opt1,opt2中,系统将尝试查找并调用Agent_OnLoad_foo启动例程。
使用-agentlib:-agentpath:加载的库将被搜索以便于JNI本地方法实现,以便在工具中使用Java编程语言代码,这对于字节码注入是必要的。
在搜索所有其他库之后将搜索代理库(希望覆盖或拦截非代理方法的本地方法实现的代理可以使用NativeMethodBind事件)。
这些开关只执行上述操作,不会更改VM或JVM的状态。不需要命令行选项来启用JVM或JVM的某些方面,这是通过功能的程序化使用来处理的。

代理启动

VM通过调用启动函数来启动每个代理。如果代理在OnLoad阶段启动,则将调用函数Agent_OnLoad或静态链接代理的Agent_OnLoad_L。如果代理在活动阶段启动,则将调用函数Agent_OnAttach或静态链接代理的Agent_OnAttach_L。每个代理只调用一次启动函数。

代理启动(OnLoad阶段)

如果代理在OnLoad阶段启动,则其代理库必须导出具有以下原型的启动函数:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
或对于名为'L'的静态链接代理:
JNIEXPORT jint JNICALL
Agent_OnLoad_L(JavaVM *vm, char *options, void *reserved)
VM将通过调用此函数来启动代理。它将在VM初始化的早期调用,以便:
虚拟机将调用 Agent_OnLoadAgent_OnLoad_<agent-lib-name> 函数,并将 <options> 作为第二个参数传递 - 也就是说,使用命令行选项示例, "opt1,opt2" 将传递给 char *options 参数的 Agent_OnLoadoptions 参数被编码为 修改后的 UTF-8 字符串。如果未指定 =<options>,则传递一个长度为零的字符串给 optionsoptions 字符串的生命周期是 Agent_OnLoadAgent_OnLoad_<agent-lib-name> 调用。如果在此时间之后仍需要该字符串或字符串的部分,则必须复制。从 Agent_OnLoad 被调用到返回的时间段称为 OnLoad 阶段。由于虚拟机在 OnLoad 阶段期间尚未初始化,因此在 Agent_OnLoad 内允许的操作集受到限制(请参阅此时可用功能的功能描述)。代理可以安全地处理选项并使用 SetEventCallbacks 设置事件回调。一旦接收到虚拟机初始化事件(即调用 VMInit 回调),代理就可以完成其初始化。

原因:早期启动是必需的,以便代理可以设置所需的功能,其中许多功能必须在虚拟机初始化之前设置。在 JVMDI 中,-Xdebug 命令行选项提供了非常粗粒度的功能控制。JVMPI 实现使用各种技巧提供单个“JVMPI on”开关。没有合理的命令行选项可以提供所需功能与性能影响之间所需的精细控制。早期启动还是必需的,以便代理可以控制执行环境 - 修改文件系统和系统属性以安装其功能。

Agent_OnLoadAgent_OnLoad_<agent-lib-name> 的返回值用于指示错误。除零以外的任何值均表示错误并导致虚拟机终止。

代理启动(活动阶段)

虚拟机可能支持一种机制,允许代理在虚拟机的活动 阶段中启动。支持方式的详细信息是特定于实现的。例如,工具可能使用某些特定于平台的机制或特定于实现的 API,附加到运行中的虚拟机,并请求其启动给定代理。
虚拟机会在标准错误流上为每个尝试在活动阶段启动的代理打印警告。如果先前已启动代理(在 OnLoad 阶段或活动阶段),则在尝试第二次或后续次启动相同代理时是否打印警告是特定于实现的。警告可以通过特定于实现的命令行选项禁用。
实现注意: 对于 HotSpot 虚拟机,VM 选项 -XX:+EnableDynamicAgentLoading 用于选择允许在活动阶段动态加载代理。此选项抑制了在活动阶段启动代理时向标准错误打印警告。
如果在活动阶段启动代理,则其代理库必须导出具有以下原型的启动函数:
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char *options, void *reserved)
或对于名为 'L' 的静态链接代理:
JNIEXPORT jint JNICALL
Agent_OnAttach_L(JavaVM* vm, char *options, void *reserved)
虚拟机将通过调用此函数来启动代理。它将在附加到虚拟机的线程的上下文中调用。第一个参数 <vm> 是 Java VM。 <options> 参数是提供给代理的启动选项。 <options> 被编码为 修改后的 UTF-8 字符串。如果未提供启动选项,则传递一个长度为零的字符串给 optionsoptions 字符串的生命周期是 Agent_OnAttachAgent_OnAttach_<agent-lib-name> 调用。如果在此时间之后仍需要该字符串或字符串的部分,则必须复制。
请注意,一些 功能 在活动阶段可能不可用。
Agent_OnAttachAgent_OnAttach_<agent-lib-name > 函数初始化代理并返回一个值给虚拟机,指示是否发生错误。除零以外的任何值表示错误。错误不会导致虚拟机终止。相反,虚拟机会忽略错误,或者采取某些特定于实现的操作--例如,可能会将错误打印到标准错误,或者在系统日志中记录错误。

代理卸载

库可以选择导出具有以下原型的卸载函数:
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm)
或对于名为 'L' 的静态链接代理:
JNIEXPORT void JNICALL
Agent_OnUnload_L(JavaVM *vm)
当库即将被卸载时,虚拟机将调用此函数。库将被卸载(除非它被静态链接到可执行文件中),并且如果某些特定于平台的机制导致卸载(本文档中未指定卸载机制),或者库被(实际上)由虚拟机的终止卸载,则将调用此函数。虚拟机终止包括正常终止和虚拟机失败,包括启动失败,但当然不包括不受控制的关闭。如果 Agent_OnAttach/ Agent_OnAttach_L 函数报告错误(返回非零值),实现也可以选择不调用此函数。请注意此函数与 VM 死亡事件之间的区别:要发送 VM 死亡事件,虚拟机必须至少运行到初始化点,并存在已设置 VMDeath 回调并启用事件的有效 JVM TI 环境。这些对于 Agent_OnUnloadAgent_OnUnload_<agent-lib-name> 都不是必需的,如果库由于其他原因被卸载,则也会调用此函数。在发送 VM 死亡事件之前,将调用此函数(假设由于 VM 终止而调用此函数)。此函数可用于清理代理分配的资源。

JAVA_TOOL_OPTIONS

由于无法始终访问或修改命令行,例如在嵌入式虚拟机中或仅在脚本深处启动的虚拟机中,提供了一个 JAVA_TOOL_OPTIONS 变量,以便代理可以在这些情况下启动。
支持环境变量或其他命名字符串的平台可能支持 JAVA_TOOL_OPTIONS 变量。此变量将在空格边界处分隔为选项。空格字符包括空格、制表符、回车、换行、垂直制表符和换页符。连续的空格字符被视为等同于单个空格字符。除非用引号引起来,否则选项中不包含空格。引号如下: JNI_CreateJavaVM(在 JNI 调用 API 中)将这些选项前置到其 JavaVMInitArgs 参数中提供的选项之前。在安全性成为问题的情况下,平台可以禁用此功能;例如,参考实现在 Unix 系统上当有效用户或组 ID 与实际 ID 不同时会禁用此功能。此功能旨在支持工具的初始化--特别包括启动本地或 Java 编程语言代理。多个工具可能希望使用此功能,因此不应覆盖变量,而应将选项附加到变量。请注意,由于变量在 JNI 调用 API 创建 VM 调用时处理,因此由启动器处理的选项(例如,VM 选择选项)将不会被处理。

环境

JVM TI 规范支持多个同时存在的 JVM TI 代理。每个代理都有自己的 JVM TI 环境。也就是说,每个代理的 JVM TI 状态是独立的 - 对一个环境的更改不会影响其他环境。一个 JVM TI 环境的状态包括: 尽管它们的 JVM TI 状态是独立的,代理检查和修改 VM 的共享状态,它们还共享它们执行的本机环境。因此,代理可以扰乱其他代理的结果或导致它们失败。代理编写者有责任指定与其他代理的兼容性级别。 JVM TI实现无法阻止代理之间的破坏性交互。减少这些事件发生可能性的技术超出了本文档的范围。
代理通过将JVM版本作为JNI调用API函数 GetEnv的接口ID来创建JVM TI环境。有关创建和使用JVM TI环境的更多详细信息,请参阅访问JVM TI函数。通常,通过从Agent_OnLoad调用GetEnv来创建JVM TI环境。

字节码插装

此接口不包括一些人们可能期望在具有性能分析支持的接口中看到的事件。一些示例包括完整速度的方法进入和退出事件。相反,该接口提供了对字节码插装的支持,即修改构成目标程序的Java虚拟机字节码指令的能力。通常,这些修改是为了向方法的代码添加“事件” - 例如,在方法的开头添加一个调用MyProfiler.methodEntered()的调用。由于更改仅是添加性的,它们不会修改应用程序状态或行为。由于插入的代理代码是标准字节码,因此VM可以以全速运行,不仅优化目标程序,还优化插装。如果插装不涉及从字节码执行切换,则不需要昂贵的状态转换。结果是高性能事件。这种方法还为代理提供了完全控制:插装可以限制在代码的“有趣”部分(例如,最终用户的代码)并且可以是有条件的。插装可以完全在Java编程语言代码中运行,也可以调用本机代理。插装可以简单地维护计数器,也可以对事件进行统计采样。
可以通过以下三种方式插入插装:
此接口提供的类修改功能旨在提供插装机制(ClassFileLoadHook事件和RetransformClasses函数),以及在开发期间进行修复和继续调试(RedefineClasses函数)。
在插装核心类时必须小心避免干扰依赖关系。例如,获取每个对象分配通知的一种方法是对Object的构造函数进行插装。假设构造函数最初为空,则构造函数可以更改为:
      public Object() {
        MyProfiler.allocationTracker(this);
      }
    
但是,如果使用ClassFileLoadHook事件进行此更改,则可能会影响典型VM的方式:第一个创建的对象将调用构造函数,导致加载MyProfiler;然后导致对象创建,由于MyProfiler尚未加载,会导致无限递归;从而导致堆栈溢出。对此的改进是延迟调用跟踪方法直到安全时间。例如,trackAllocations可以在VMInit事件的处理程序中设置。
      static boolean trackAllocations = false;

      public Object() {
        if (trackAllocations) {
          MyProfiler.allocationTracker(this);
        }
      }
    
SetNativeMethodPrefix允许通过包装方法来插装本地方法。

模块中代码的字节码插装

代理可以使用函数AddModuleReadsAddModuleExportsAddModuleOpensAddModuleUsesAddModuleProvides来更新模块,扩展它读取的模块集、导出或开放给其他模块的包集,或使用和提供的服务。
为了帮助在引导类加载器的搜索路径或加载主类的类加载器的搜索路径上部署支持类的代理,Java虚拟机安排通过ClassFileLoadHook事件转换的类的模块读取两个类加载器的未命名模块。

修改的UTF-8字符串编码

JVM TI使用修改的UTF-8来编码字符字符串。这与JNI使用的编码相同。修改的UTF-8在表示补充字符和空字符方面与标准UTF-8不同。有关详细信息,请参阅JNI规范的修改的UTF-8字符串部分。

规范上下文

由于此接口提供对在Java虚拟机中运行的应用程序状态的访问;术语是指Java平台而不是本机平台(除非另有说明)。例如:
Sun、Sun Microsystems、Sun标志、Java和JVM是Oracle及其关联公司在美国和其他国家的商标或注册商标。


函数

访问函数

本机代码通过调用JVM TI函数来访问JVM TI功能。访问JVM TI函数是通过与Java本机接口(JNI)函数访问方式相同的接口指针来实现的。JVM TI接口指针称为环境指针
环境指针是指向环境的指针,类型为jvmtiEnv*。环境包含有关其JVM TI连接的信息。环境中的第一个值是指向函数表的指针。函数表是指向JVM TI函数的指针数组。每个函数指针位于数组内的预定义偏移量处。
在C语言中使用时:使用双重间接访问函数;环境指针提供上下文,并且是每个函数调用的第一个参数;例如:
jvmtiEnv *jvmti;
...
jvmtiError err = (*jvmti)->GetLoadedClasses(jvmti, &class_count, &classes);
    
在C++语言中使用时:函数作为jvmtiEnv的成员函数访问;环境指针不会传递给函数调用;例如:
jvmtiEnv *jvmti;
...
jvmtiError err = jvmti->GetLoadedClasses(&class_count, &classes);
    
除非另有说明,本规范中的所有示例和声明均使用C语言。
可以通过JNI调用APIGetEnv函数获取JVM TI环境:
jvmtiEnv *jvmti;
...
(*jvm)->GetEnv(jvm, &jvmti, JVMTI_VERSION_1_0);
    
每次调用GetEnv都会创建一个新的JVM TI连接,因此会创建一个新的JVM TI环境。 GetEnvversion参数必须是JVM TI版本。返回的环境可能与请求的版本不同,但返回的环境必须是兼容的。GetEnv将返回JNI_EVERSION如果没有可用的兼容版本,如果不支持JVM TI或JVM TI在当前VM配置中不受支持。其他接口可能会添加用于在特定上下文中创建JVM TI环境的接口。每个环境都有自己的状态(例如,所需事件事件处理函数功能)。使用DisposeEnvironment释放环境。。因此,与JNI每个线程一个环境不同,JVM TI环境跨线程工作,并动态创建。

函数返回值

JVM TI函数始终通过错误代码返回jvmtiError函数返回值。一些函数可以通过调用函数提供的指针返回附加值。在某些情况下,JVM TI函数分配内存,您的程序必须显式释放该内存。这在各个JVM TI函数描述中有指示。空列表、数组、序列等将作为NULL返回。
如果JVM TI函数遇到错误(任何返回值不是JVMTI_ERROR_NONE),则由参数指针引用的内存值是未定义的,但不会分配任何内存,也不会分配任何全局引用。如果错误是由于无效输入导致的,则不会发生任何操作。

管理JNI对象引用

JVM TI函数使用JNI引用(jobjectjclass)及其派生类(jthreadjthreadGroup)标识对象。传递给JVM TI函数的引用可以是全局的或局部的,但它们必须是强引用。JVM TI函数返回的所有引用都是局部引用--这些局部引用是在JVM TI调用期间创建的。局部引用是必须管理的资源(请参阅JNI文档)。当线程从本机代码返回时,所有局部引用都将被释放。请注意,包括典型代理线程在内的一些线程永远不会从本机代码返回。确保线程能够创建十六个局部引用而无需任何显式管理。对于在返回本机代码之前执行有限数量的JVM TI调用的线程(例如,处理事件的线程),可能确定不需要任何显式管理。然而,长时间运行的代理线程将需要显式的局部引用管理--通常使用JNI函数PushLocalFramePopLocalFrame。相反,为了在从本机代码返回后保留引用,它们必须转换为全局引用。这些规则不适用于jmethodIDjfieldID,因为它们不是jobject

调用函数的先决状态

除非函数明确说明代理必须将线程或VM带到特定状态(例如,暂停),否则JVM TI实现负责将VM带到安全和一致的状态以执行函数。

异常和函数

JVM TI函数永远不会抛出异常;错误条件通过函数返回值进行通信。任何现有的异常状态在调用JVM TI函数时保留。有关处理异常的信息,请参阅JNI规范中的Java异常部分。

函数索引


内存管理

内存管理函数: