为您推荐:
精华内容
最热下载
问答
  • 16.25MB qq_25421165 2020-03-29 00:47:21
  • 8KB janesshang 2018-12-13 07:04:02
  • 4星
    81KB weixin_43531768 2019-03-02 22:42:59
  • 181KB weixin_38528888 2021-03-02 21:59:52
  • 4.92MB weixin_42139871 2021-05-17 05:01:07
  • 452KB weixin_42140716 2021-06-22 13:37:22
  • jvmti 今天,我想谈一谈我们大多数人每天都不会看到和使用的另一种Java,更确切地说,是有关较低级别的绑定,一些本机代码以及如何执行一些小的魔术。 尽管我们不会在JVM上找到真正的魔力源,但是在单个帖子的范围...

    jvmti

    今天,我想谈一谈我们大多数人每天都不会看到和使用的另一种Java,更确切地说,是有关较低级别的绑定,一些本机代码以及如何执行一些小的魔术。 尽管我们不会在JVM上找到真正的魔力源,但是在单个帖子的范围内可以实现一些小奇迹。

    我花了很多时间在ZeroTurnaroundRebelLabs团队中进行研究,编写和编码,该公司为Java开发人员创建工具,这些工具主要以javaagents的身份运行。 通常情况下,如果您想在不重写JVM的情况下增强JVM或在JVM上获得任何强大的功能,则必须深入研究Java代理的美丽世界。 它们有两种形式:Java javaagents和本机Javaagents。 在这篇文章中,我们将集中讨论后者。


    注意, XRebel产品负责人Anton Arhipov的这个GeeCON Prague演示文稿是学习完全用Java编写的Javaagents的一个很好的起点: 与Javassist一起玩

    在本文中,我们将创建一个小型的本机JVM代理,探讨将本机方法公开到Java应用程序中的可能性,并了解如何利用Java虚拟机工具接口

    如果您正在寻找帖子的实用内容,我们将能够在扰乱警报的情况下计算堆中存在给定类的实例数量。

    想象一下,您是圣诞老人值得信赖的黑客精灵,而这位大人物对您来说面临以下挑战:

    圣诞老人: 我亲爱的Hacker Elf,您能否编写一个程序来指出JVM堆中当前隐藏了多少个Thread对象?

    另一个不愿意挑战自己的小精灵会回答: 这很容易直接,对吗?

    return Thread.getAllStackTraces().size();

    但是,如果我们想对我们的解决方案进行过度设计以能够回答有关任何给定类的问题,该怎么办? 说我们要实现以下接口?

    public interface HeapInsight {
      int countInstances(Class klass);
    }

    是的,那是不可能的,对吧? 如果您收到String.class作为参数怎么办? 不用担心,我们只需要更深入地研究JVM的内部结构。 JVMTI作者可以使用的一件事是JVMTI (Java虚拟机工具接口)。 它是很久以前添加的,许多看似神奇的工具都在使用它。 JVMTI提供了两件事:

    • 本机API
    • 一种工具API,用于监视和转换装入JVM的类的字节码。

    就我们的示例而言,我们需要访问本机API。 我们要使用的是IterateThroughHeap函数,该函数使我们可以提供一个自定义回调,以对给定类的每个对象执行该回调。

    首先,让我们创建一个本地代理,该代理将加载和回显某些内容,以确保我们的基础架构能够正常工作。

    本机代理程序是用C / C ++编写的,并被编译成动态库,以便在我们甚至开始考虑Java之前就进行加载。 如果您不精通C ++,请不要担心,没有很多精灵,也不会很难。 我的C ++方法包括2种主要策略:巧合编程和避免段错误。 因此,由于我设法编写了这篇文章的示例代码并对其进行了注释,因此我们可以一起研究一下。 注意:以上段落应作为免责声明,请勿将此代码置于任何对您有价值的环境中。

    这是创建第一个本机代理的方法:

    #include 
    #include 
     
    using namespace std;
     
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
    {
      cout << "A message from my SuperAgent!" << endl;
      return JNI_OK;
    }

    该声明的重要部分是声明一个名为Agent_OnLoad的函数,该函数遵循动态链接的代理的文档

    将文件另存为例如native-agent.cpp ,让我们看看我们可以做些什么来变成一个库。

    我在OSX上,因此我使用clang对其进行编译,以节省一些时间,下面是完整的命令:

    clang -shared -undefined dynamic_lookup -o agent.so -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/ -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/darwin native-agent.cpp

    这将创建一个agent.so文件,该文件是可以为我们服务的库。 为了测试它,让我们创建一个虚拟的hello world Java类。

    package org.shelajev;
    public class Main {
       public static void main(String[] args) {
           System.out.println("Hello World!");
       }
    }

    当你用正确的-agentpath选项指向agent.so运行它,你应该看到下面的输出:

    java -agentpath:agent.so org.shelajev.Main
    A message from my SuperAgent!
    Hello World!

    很好! 现在,我们拥有一切使之真正有用的地方。 首先,我们需要一个jvmtiEnv实例,当我们位于Agent_OnLoad中时 ,可以通过JavaVM * jvm获得该实例 ,但以后将不可用。 因此,我们必须将其存储在全球可访问的位置。 我们通过声明一个全局结构来存储它。

    #include 
    #include 
     
    using namespace std;
     
    typedef struct {
     jvmtiEnv *jvmti;
    } GlobalAgentData;
     
    static GlobalAgentData *gdata;
     
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
    {
      jvmtiEnv *jvmti = NULL;
      jvmtiCapabilities capa;
      jvmtiError error;
      
      // put a jvmtiEnv instance at jvmti.
      jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
      if (result != JNI_OK) {
        printf("ERROR: Unable to access JVMTI!\n");
      }
      // add a capability to tag objects
      (void)memset(∩a, 0, sizeof(jvmtiCapabilities));
      capa.can_tag_objects = 1;
      error = (jvmti)->AddCapabilities(∩a);
     
      // store jvmti in a global data
      gdata = (GlobalAgentData*) malloc(sizeof(GlobalAgentData));
      gdata->jvmti = jvmti;
      return JNI_OK;
    }

    我们还更新了代码,以添加标记对象的功能,这是我们遍历堆所需的。 现在准备工作已经完成,我们已经初始化了JVMTI实例并且可供我们使用。 让我们通过JNI将其提供给我们的Java代码。

    JNI代表Java本机接口 ,这是将本机代码调用包含到Java应用程序中的一种标准方式。 Java部分将非常简单明了,将以下countInstances方法定义添加到Main类:

    package org.shelajev;
    
    public class Main {
       public static void main(String[] args) {
           System.out.println("Hello World!");
           int a = countInstances(Thread.class);
           System.out.println("There are " + a + " instances of " + Thread.class);
       }
    
       private static native int countInstances(Class klass);
    }

    为了适应本机方法,我们必须更改本机代理代码。 我将在稍后解释,但现在在其中添加以下函数定义:

    extern "C"
    JNICALL jint objectCountingCallback(jlong class_tag, jlong size, jlong* tag_ptr, jint length, void* user_data) 
    {
     int* count = (int*) user_data;
     *count += 1; 
     return JVMTI_VISIT_OBJECTS;
    }
     
    extern "C"
    JNIEXPORT jint JNICALL Java_org_shelajev_Main_countInstances(JNIEnv *env, jclass thisClass, jclass klass) 
    {
     int count = 0;
       jvmtiHeapCallbacks callbacks;
    (void)memset(&callbacks, 0, sizeof(callbacks));
    callbacks.heap_iteration_callback = &objectCountingCallback;
     jvmtiError error = gdata->jvmti->IterateThroughHeap(0, klass, &callbacks, &count);
     return count;
    }

    Java_org_shelajev_Main_countInstances在这里更有趣,它的名称遵循约定,以Java_开头,然后是_分隔的完全限定的类名,然后是Java代码中的方法名。 另外,请不要忘记JNIEXPORT声明,该声明指出该函数已导出到Java世界中。

    Java_org_shelajev_Main_countInstances内部,我们将objectCountingCallback函数指定为回调,并使用Java应用程序中的参数调用IterateThroughHeap

    请注意,我们的本机方法是静态的,因此C对应项中的参数为:

    JNIEnv *env, jclass thisClass, jclass klass

    对于实例方法,它们将有所不同:

    JNIEnv *env, jobj thisInstance, jclass klass

    这里的thisInstance指向Java方法调用的this对象。

    现在, objectCountingCallback的定义直接来自文档 。 身体无非就是增加一个int。

    繁荣! 全做完了! 感谢您的耐心等待。 如果您仍在阅读本文,则可以测试上面的所有代码。

    再次编译本机代理并运行Main类。 这是我看到的:

    java -agentpath:agent.so org.shelajev.Main
    Hello World!
    There are 7 instances of class java.lang.Thread

    如果我添加一个线程t = new Thread(); 行到main方法,我在堆上看到8个实例。 听起来好像真的可行。 您的线程数几乎肯定会有所不同,不用担心,这是正常现象,因为它确实计入了JVM簿记线程,进行编译,GC等操作。

    现在,如果我要计算堆上String实例的数量,只需更改参数类即可。 我希望圣诞老人是一个真正通用的解决方案。

    哦,如果您有兴趣,它会为我找到2423个String实例。 对于小型应用程序来说,这个数字相当高。 也,

    return Thread.getAllStackTraces().size();

    给我5个而不是8个,因为它不包括簿记线程! 谈论琐碎的解决方案,是吗?

    现在,您已经掌握了这些知识,并且知道了本教程,并不是说您已经准备好编写自己的JVM监视或增强工具,但这绝对是一个开始。

    在本文中,我们从零开始编写了本机Java代理,该代理成功编译,加载和运行。 它使用JVMTI来获取无法通过其他方式访问的JVM的见解。 相应的Java代码调用本机库并解释结果。

    这通常是最神奇的JVM工具所采用的方法,我希望其中的一些魔术已为您揭开神秘面纱。

    翻译自: https://www.javacodegeeks.com/2014/12/own-your-heap-iterate-class-instances-with-jvmti.html

    jvmti

    展开全文
    dnc8371 2020-06-19 07:25:52
  • JVMTI 错误处理 JVMTI 沿用了基本的错误处理方式,即使用返回的错误代码通知当前的错误,几乎所有的 JVMTI 函数调用都具有以下模式: jvmtiError err = jvmti->someJVMTImethod (somePara … ); 其中 err 就是返回...

    此内容是该系列 4 部分中的第 2 部分: 深入 Java 调试体系

    Java 程序的诊断和调试

    开发人员对 Java 程序的诊断和调试有许多不同种类、不同层次的需求,这就使得开发人员需要使用不同的工具来解决问题。比如,在 Java 程序运行的过程中,程序员希望掌握它总体的运行状况,这个时候程序员可以直接使用 JDK 提供的 jconsole 程序。如果希望提高程序的执行效率,开发人员可以使用各种 Java Profiler。这种类型的工具非常多,各有优点,能够帮助开发人员找到程序的瓶颈,从而提高程序的运行速度。开发人员还会遇到一些与内存相关的问题,比如内存占用过多,大量内存不能得到释放,甚至导致内存溢出错误(OutOfMemoryError)等等,这时可以把当前的内存输出到 Dump 文件,再使用堆分析器或者 Dump 文件分析器等工具进行研究,查看当前运行态堆(Heap)中存在的实例整体状况来诊断问题。所有这些工具都有一个共同的特点,就是最终他们都需要通过和虚拟机进行交互,来发现 Java 程序运行的问题。

    已有的这些工具虽然强大易用,但是在一些高级的应用环境中,开发者常常会有一些特殊的需求,这个时候就需要定制工具来达成目标。 JDK 本身定义了目标明确并功能完善的 API 来与虚拟机直接交互,而且这些 API 能很方便的进行扩展,从而满足开发者各式的需求。在本文中,将比较详细地介绍 JVMTI,以及如何使用 JVMTI 编写一个定制的 Agent 。

    Agent

    Agent 即 JVMTI 的客户端,它和执行 Java 程序的虚拟机运行在同一个进程上,因此通常他们的实现都很紧凑,他们通常由另一个独立的进程控制,充当这个独立进程和当前虚拟机之间的中介,通过调用 JVMTI 提供的接口和虚拟机交互,负责获取并返回当前虚拟机的状态或者转发控制命令。

    JVMTI 的简介

    JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的更新版本。从这个 API 的发展历史轨迹中我们就可以知道,JVMTI 提供了可用于 debug 和 profiler 的接口;同时,在 Java 5/6 中,虚拟机接口也增加了监听(Monitoring),线程分析(Thread analysis)以及覆盖率分析(Coverage Analysis)等功能。正是由于 JVMTI 的强大功能,它是实现 Java 调试器,以及其它 Java 运行态测试与分析工具的基础。

    JVMTI 并不一定在所有的 Java 虚拟机上都有实现,不同的虚拟机的实现也不尽相同。不过在一些主流的虚拟机中,比如 Sun 和 IBM,以及一些开源的如 Apache Harmony DRLVM 中,都提供了标准 JVMTI 实现。

    JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。

    -agentlib:agent-lib-name=options

    -agentpath:path-to-agent=options

    Agent 的工作过程

    启动

    Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:

    所有的 Java 类都未被初始化;

    所有的对象实例都未被创建;

    因而,没有任何 Java 代码被执行;

    但在这个时候,我们已经可以:

    操作 JVMTI 的 Capability 参数;

    使用系统参数;

    动态库被加载之后,虚拟机会先寻找一个 Agent 入口函数:

    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)

    在这个函数中,虚拟机传入了一个 JavaVM 指针,以及命令行的参数。通过 JavaVM,我们可以获得 JVMTI 的指针,并获得 JVMTI 函数的使用能力,所有的 JVMTI 函数都通过这个 jvmtiEnv 获取,不同的虚拟机实现提供的函数细节可能不一样,但是使用的方式是统一的。

    jvmtiEnv *jvmti;

    (*jvm)->GetEnv(jvm, &jvmti, JVMTI_VERSION_1_0);

    这里传入的版本信息参数很重要,不同的 JVMTI 环境所提供的功能以及处理方式都可能有所不同,不过它在同一个虚拟机中会保持不变(有心的读者可以去比较一下 JNI 环境)。命令行参数事实上就是上面启动命令行中的 options 部分,在 Agent 实现中需要进行解析并完成后续处理工作。参数传入的字符串仅仅在 Agent_OnLoad 函数里有效,如果需要长期使用,开发者需要做内存的复制工作,同时在最后还要释放这块存储。另外,有些 JDK 的实现会使用 JAVA_TOOL_OPTIONS 所提供的参数,这个常见于一些嵌入式的 Java 虚拟机(不使用命令行)。需要强调的是,这个时候由于虚拟机并未完成初始化工作,并不是所有的 JVMTI 函数都可以被使用。

    Agent 还可以在运行时加载,如果您了解 Java Instrument 模块(可以参考这篇文章),您一定对它的运行态加载有印象,这个新功能事实上也是 Java Agent 的一个实现。具体说来,虚拟机会在运行时监听并接受 Agent 的加载,在这个时候,它会使用 Agent 的:

    JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);

    同样的在这个初始化阶段,不是所有的 JVMTI 的 Capability 参数都处于可操作状态,而且 options 这个 char 数组在这个函数运行之后就会被丢弃,如果需要,需要做好保留工作。

    Agent 的主要功能是通过一系列的在虚拟机上设置的回调(callback)函数完成的,一旦某些事件发生,Agent 所设置的回调函数就会被调用,来完成特定的需求。

    卸载

    最后,Agent 完成任务,或者虚拟机关闭的时候,虚拟机都会调用一个类似于类析构函数的方法来完成最后的清理任务,注意这个函数和虚拟机自己的 VM_DEATH 事件是不同的。

    JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)

    JVMTI 的环境和错误处理

    我们使用 JVMTI 的过程,主要是设置 JVMTI 环境,监听虚拟机所产生的事件,以及在某些事件上加上我们所希望的回调函数。

    JVMTI 环境

    我们可以通过操作 jvmtiCapabilities 来查询、增加、修改 JVMTI 的环境参数。当然,对于每一个不同的虚拟机来说,基于他们的实现不尽相同,导致了 JVMTI 的环境也不一定一致。标准的 jvmtiCapabilities 定义了一系列虚拟机的功能,比如 can_redefine_any_class 定义了虚拟机是否支持重定义类,can_retransform_classes 定义了是否支持在运行的时候改变类定义等等。如果熟悉 Java Instrumentation,一定不会对此感到陌生,因为 Instrumentation 就是对这些在 Java 层上的包装。对用户来说,这块最主要的是查看当前 JVMTI 环境,了解虚拟机具有的功能。要了解这个,其实很简单,只需通过对 jvmtiCapabilities 的一系列变量的考察就可以。

    err = (*jvmti)->GetCapabilities(jvmti, &capa); // 取得 jvmtiCapabilities 指针。

    if (err == JVMTI_ERROR_NONE) {

    if (capa.can_redefine_any_class) { ... }

    } // 查看是否支持重定义类

    另外,虚拟机有自己的一些功能,一开始并未被启动,那么增加或修改 jvmtiCapabilities 也是可能的,但不同的虚拟机对这个功能的处理也不太一样,多数的虚拟机允许增改,但是有一定的限制,比如仅支持在 Agent_OnLoad 时,即虚拟机启动时作出,它某种程度上反映了虚拟机本身的构架。开发人员无需要考虑 Agent 的性能和内存占用,就可以在 Agent 被加载的时候启用所有功能:

    err = (*jvmti)->GetPotentialCapabilities(jvmti, &capa); // 取得所有可用的功能

    if (err == JVMTI_ERROR_NONE) {

    err = (*jvmti)->AddCapabilities(jvmti, &capa);

    ...

    }

    最后我们要注意的是,JVMTI 的函数调用都有其时间性,即特定的函数只能在特定的虚拟机状态下才能调用,比如 SuspendThread(挂起线程)这个动作,仅在 Java 虚拟机处于运行状态(live phase)才能调用,否则导致一个内部异常。

    JVMTI 错误处理

    JVMTI 沿用了基本的错误处理方式,即使用返回的错误代码通知当前的错误,几乎所有的 JVMTI 函数调用都具有以下模式:

    jvmtiError err = jvmti->someJVMTImethod (somePara … );

    其中 err 就是返回的错误代码,不同函数的错误信息可以在 Java 规范里查到。

    JVMTI 基本功能

    JVMTI 的功能非常丰富,包含了虚拟机中线程、内存 / 堆 / 栈,类 / 方法 / 变量,事件 / 定时器处理等等 20 多类功能,下面我们介绍一下,并举一些简单列子。

    事件处理和回调函数

    从上文我们知道,使用 JVMTI 一个基本的方式就是设置回调函数,在某些事件发生的时候触发并作出相应的动作。因此这一部分的功能非常基本,当前版本的 JVMTI 提供了许多事件(Event)的回调,包括虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等。如果想对这些事件进行处理,我们需要首先为该事件写一个函数,然后在 jvmtiEventCallbacks 这个结构中指定相应的函数指针。比如,我们对线程启动感兴趣,并写了一个 HandleThreadStart 函数,那么我们需要在 Agent_OnLoad 函数里加入:

    jvmtiEventCallbacks eventCallBacks;

    memset(&ecbs, 0, sizeof(ecbs)); // 初始化

    eventCallBacks.ThreadStart = &HandleThreadStart; // 设置函数指针

    ...

    在设置了这些回调之后,就可以调用下述方法,来最终完成设置。在接下来的虚拟机运行过程中,一旦有线程开始运行发生,虚拟机就会回调 HandleThreadStart 方法。

    jvmti->SetEventCallbacks(eventCallBacks, sizeof(eventCallBacks));

    设置回调函数的时候,开发者需要注意以下几点:

    如同 Java 异常机制一样,如果在回调函数中自己抛出一个异常(Exception),或者在调用 JNI 函数的时候制造了一些麻烦,让 JNI 丢出了一个异常,那么任何在回调之前发生的异常就会丢失,这就要求开发人员要在处理错误的时候需要当心。

    虚拟机不保证回调函数会被同步,换句话说,程序有可能同时运行同一个回调函数(比如,好几个线程同时开始运行了,这个 HandleThreadStart 就会被同时调用几次),那么开发人员在开发回调函数时需要处理同步的问题。

    内存控制和对象获取

    内存控制是一切运行态的基本功能。 JVMTI 除了提供最简单的内存申请和撤销之外(这块内存不受 Java 堆管理,开发人员需要自行进行清理工作,不然会造成内存泄漏),也提供了对 Java 堆的操作。众所周知,Java 堆中存储了 Java 的类、对象和基本类型(Primitive),通过对堆的操作,开发人员可以很容易的查找任意的类、对象,甚至可以强行执行垃圾收集工作。 JVMTI 中对 Java 堆的操作与众不同,它没有提供一个直接获取的方式(由此可见,虚拟机对对象的管理并非是哈希表,而是某种树 / 图方式),而是使用一个迭代器(iterater)的方式遍历:

    jvmtiError FollowReferences(jvmtiEnv* env,

    jint heap_filter,

    jclass klass,

    jobject initial_object,// 该方式可以指定根节点

    const jvmtiHeapCallbacks* callbacks,// 设置回调函数

    const void* user_data)

    或者

    jvmtiError IterateThroughHeap(jvmtiEnv* env,

    jint heap_filter,

    jclass klass,

    const jvmtiHeapCallbacks* callbacks,

    const void* user_data)// 遍历整个 heap

    在遍历的过程中,开发者可以设定一定的条件,比如,指定是某一个类的对象,并设置一个回调函数,如果条件被满足,回调函数就会被执行。开发者可以在回调函数中对当前传回的指针进行打标记(tag)操作——这又是一个特殊之处,在第一遍遍历中,只能对满足条件的对象进行 tag ;然后再使用 GetObjectsWithTags 函数,获取需要的对象。

    jvmtiError GetObjectsWithTags(jvmtiEnv* env,

    jint tag_count,

    const jlong* tags, // 设定特定的 tag,即我们上面所设置的

    jint* count_ptr,

    jobject** object_result_ptr,

    jlong** tag_result_ptr)

    如果你仅仅想对特定 Java 对象操作,应该避免设置其他类型的回调函数,否则会影响效率,举例来说,多增加一个 primitive 的回调函数,可能会使整个操作效率下降一个数量级。

    线程和锁

    线程是 Java 运行态中非常重要的一个部分,在 JVMTI 中也提供了很多 API 进行相应的操作,包括查询当前线程状态,暂停,恢复或者终端线程,还可以对线程锁进行操作。开发者可以获得特定线程所拥有的锁:

    jvmtiError GetOwnedMonitorInfo(jvmtiEnv* env,

    jthread thread,

    jint* owned_monitor_count_ptr,

    jobject** owned_monitors_ptr)

    也可以获得当前线程正在等待的锁:

    jvmtiError GetCurrentContendedMonitor(jvmtiEnv* env,

    jthread thread,

    jobject* monitor_ptr)

    知道这些信息,事实上我们也可以设计自己的算法来判断是否死锁。更重要的是,JVMTI 提供了一系列的监视器(Monitor)操作,来帮助我们在 native 环境中实现同步。主要的操作是构建监视器(CreateRawMonitor),获取监视器(RawMonitorEnter),释放监视器(RawMonitorExit),等待和唤醒监视器 (RawMonitorWait,RawMonitorNotify) 等操作,通过这些简单锁,程序的同步操作可以得到保证。

    调试功能

    调试功能是 JVMTI 的基本功能之一,这主要包括了设置断点、调试(step)等,在 JVMTI 里面,设置断点的 API 本身很简单:

    jvmtiError SetBreakpoint(jvmtiEnv* env,

    jmethodID method,

    jlocation location)

    jlocation 这个数据结构在这里代表的是对应方法方法中一个可执行代码的行数。在断点发生的时候,虚拟机会触发一个事件,开发者可以使用在上文中介绍过的方式对事件进行处理。

    JVMTI 数据结构

    JVMTI 中使用的数据结构,首先也是一些标准的 JNI 数据结构,比如 jint,jlong ;其次,JVMTI 也定义了一些基本类型,比如 jthread,表示一个 thread,jvmtiEvent,表示 jvmti 所定义的事件;更复杂的有 JVMTI 的一些需要用结构体表示的数据结构,比如堆的信息(jvmtiStackInfo)。这些数据结构在文档中都有清楚的定义,本文就不再详细解释。

    一个简单的 Agent 实现

    下面将通过一个具体的例子,来阐述如何开发一个简单的 Agent 。这个 Agent 是通过 C++ 编写的(读者可以在最后下载到完整的代码),他通过监听 JVMTI_EVENT_METHOD_ENTRY 事件,注册对应的回调函数来响应这个事件,来输出所有被调用函数名。有兴趣的读者还可以参照这个基本流程,通过 JVMTI 提供的丰富的函数来进行扩展和定制。

    Agent 的设计

    具体实现都在 MethodTraceAgent 这个类里提供。按照顺序,他会处理环境初始化、参数解析、注册功能、注册事件响应,每个功能都被抽象在一个具体的函数里。

    class MethodTraceAgent

    {

    public:

    void Init(JavaVM *vm) const throw(AgentException);

    void ParseOptions(const char* str) const throw(AgentException);

    void AddCapability() const throw(AgentException);

    void RegisterEvent() const throw(AgentException);

    ...

    private:

    ...

    static jvmtiEnv * m_jvmti;

    static char* m_filter;

    };

    Agent_OnLoad 函数会在 Agent 被加载的时候创建这个类,并依次调用上述各个方法,从而实现这个 Agent 的功能。

    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)

    {

    ...

    MethodTraceAgent* agent = new MethodTraceAgent();

    agent->Init(vm);

    agent->ParseOptions(options);

    agent->AddCapability();

    agent->RegisterEvent();

    ...

    }

    运行过程如图 1 所示:

    1dda77386e770a1564917f4cbc401152.png

    图 1. Agent 时序图

    Agent 编译和运行

    Agent 的编译非常简单,他和编译普通的动态链接库没有本质区别,只是需要将 JDK 提供的一些头文件包含进来。

    Windows:

    cl /EHsc -I${JAVA_HOME}\include\ -I${JAVA_HOME}\include\win32

    -LD MethodTraceAgent.cpp Main.cpp -FeAgent.dll

    Linux:

    g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux

    MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libagent.so

    在附带的代码文件里提供了一个可运行的 Java 类,默认情况下运行的结果如下图所示:

    f39de21cbafceaf0e9c83d3faee553a5.png

    图 2. 默认运行输出

    现在,我们运行程序前告诉 Java 先加载编译出来的 Agent:

    java -agentlib:Agent=first MethodTraceTest

    这次的输出如图 3. 所示:

    87083ae3f146ed09af88393fb78923e2.png

    图 3. 添加 Agent 后输出

    可以当程序运行到 MethodTraceTest 的 first 方法时,Agent 会输出这个事件。“ first ”是 Agent 运行的参数,如果不指定的话,所有的进入方法的触发的事件都会被输出,如果读者把这个参数去掉再运行的话,会发现在运行 main 函数前,已经有非常多基本的类库函数被调用了。

    结语

    Java 虚拟机通过 JVMTI 提供了一整套函数来帮助用户检测管理虚拟机运行态,它主要通过 Agent 的方式实现与用户的互操作。本文简单介绍了 Agent 的实现方式和 JVMTI 的使用。通过 Agent 这种方式不仅仅用户可以使用,事实上,JDK 里面的很多工具,比如 Instrumentation 和 JDI, 都采用了这种方式。这种方式无需把这些工具绑定在虚拟机上,减少了虚拟机的负荷和内存占用。在下一篇中,我们将介绍 JDWP 如何采用 Agent 的方式,定义自己的一套通信原语,并通过多种通信方式,对外提供基本调试功能。

    【说明:原文的源代码下载链接已经失效,这里不再提供】

    展开全文
    weixin_39543835 2021-02-27 20:06:07
  • JVMTI(JVM Tool Interface)位于jpda最底层,是Java虚拟机所提供的native编程接口。JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。 JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,...

    背景描述
    JVMTI(JVM Tool Interface)位于jpda最底层,是Java虚拟机所提供的native编程接口。JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。

    JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI)。这三个模块把调试过程分解成几个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。
    JDPA 模块层次.png

    模块层次编程语言作用
    JVMTI底层C获取及控制当前虚拟机状态
    JDWP中介层C定义 JVMTI 和 JDI 交互的数据格式
    JDI高层Java提供 Java API 来远程控制被调试虚拟机

    Java 虚拟机工具接口(JVMTI)
    JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。我们知道,JVMTI 的前身是 JVMDI 和 JVMPI,它们原来分别被用于提供调试 Java 程序以及 Java 程序调节性能的功能。在 J2SE 5.0 之后 JDK 取代了 JVMDI 和 JVMPI 这两套接口,JVMDI 在最新的 Java SE 6 中已经不提供支持,而 JVMPI 也计划在 Java SE 7 后被彻底取代。

    Java 调试线协议(JDWP)
    JDWP(Java Debug Wire Protocol)是一个为 Java 调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。在 JPDA 体系中,作为前端(front-end)的调试者(debugger)进程和后端(back-end)的被调试程序(debuggee)进程之间的交互数据的格式就是由 JDWP 来描述的,它详细完整地定义了请求命令、回应数据和错误代码,保证了前端和后端的 JVMTI 和 JDI 的通信通畅。比如在 Sun 公司提供的实现中,它提供了一个名为 jdwp.dll(jdwp.so)的动态链接库文件,这个动态库文件实现了一个 Agent,它会负责解析前端发出的请求或者命令,并将其转化为 JVMTI 调用,然后将 JVMTI 函数的返回值封装成 JDWP 数据发还给后端。

    Java 调试接口(JDI)
    JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 JDI 由针对前端定义的接口组成,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行,JDI 不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务。从理论上说,开发人员只需使用 JDWP 和 JVMTI 即可支持跨平台的远程调试,但是直接编写 JDWP 程序费时费力,而且效率不高。因此基于 Java 的 JDI 层的引入,简化了操作,提高了开发人员开发调试程序的效率。

    开发简述
    基于jvmti提供的接口服务,运用C++代码(win32-add_library)在Agent_OnLoad里开发监控服务,并生成dll文件。开发完成后在java代码中加入agentpath,这样就可以监控到我们需要的信息内容。

    环境准备
    1、Dev-C++
    2、JetBrains CLion 2018.2.3
    3、IntelliJ IDEA Community Edition 2018.3.1 x64
    4、jdk1.8 64位
    5、jvmti(在jdk安装目录下jdk1.8.0_45\include里,复制到和工程案例同层级目录下)

    配置信息(路径相关修改为自己的)
    1、C++开发工具Clion配置
    1.1、配置位置;Settings->Build,Execution,Deployment->Toolchains
    1.2、MinGM配置:D:\Program Files (x86)\Dev-Cpp\MinGW64
    2、java调试时配置
    2.1、配置位置:Run/Debug Configurations ->VM options
    2.2、配置内容:-agentpath:E:\itstack\itstack.org\demo\jvmti\itstack-demo-jvmti-dll\cmake-build-debug\libitstack_demo_jvmti_dll.dll

    代码示例

    c++ 代码块:

    #include <iostream>
    #include <cstring>
    #include "jvmti.h"
    
    using namespace std;
    
    //异常回调函数
    static void JNICALL callbackException(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID methodId, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location) {
    
        // 获得方法对应的类
        jclass clazz;
        jvmti_env->GetMethodDeclaringClass(methodId, &clazz);
    
        // 获得类的签名
        char *class_signature;
        jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);
    
        //过滤非本工程类信息
        string::size_type idx;
        string class_signature_str = class_signature;
        idx = class_signature_str.find("org/itstack");
        if (idx != 1) {
            return;
        }
    
        //异常类名称
        char *exception_class_name;
        jclass exception_class = env->GetObjectClass(exception);
        jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);
    
        // 获得方法名称
        char *method_name_ptr, *method_signature_ptr;
        jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);
    
        //获取目标方法的起止地址和结束地址
        jlocation start_location_ptr;    //方法的起始位置
        jlocation end_location_ptr;      //用于方法的结束位置
        jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);
    
        //输出测试结果
        cout << "测试结果-定位类的签名:" << class_signature << endl;
        cout << "测试结果-定位方法信息:" << method_name_ptr << " -> " << method_signature_ptr << endl;
        cout << "测试结果-定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
        cout << "测试结果-异常类的名称:" << exception_class_name << endl;
    
        cout << "测试结果-输出异常信息(可以分析行号):" << endl;
        jclass throwable_class = (*env).FindClass("java/lang/Throwable");
        jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
        (*env).CallVoidMethod(exception, print_method);
    
    }
    
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
        jvmtiEnv *gb_jvmti = nullptr;
        //初始化
        vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
        // 创建一个新的环境
        jvmtiCapabilities caps;
        memset(&caps, 0, sizeof(caps));
        caps.can_signal_thread = 1;
        caps.can_get_owned_monitor_info = 1;
        caps.can_generate_method_entry_events = 1;
        caps.can_generate_exception_events = 1;
        caps.can_generate_vm_object_alloc_events = 1;
        caps.can_tag_objects = 1;
        // 设置当前环境
        gb_jvmti->AddCapabilities(&caps);
        // 创建一个新的回调函数
        jvmtiEventCallbacks callbacks;
        memset(&callbacks, 0, sizeof(callbacks));
        //异常回调
        callbacks.Exception = &callbackException;
        // 设置回调函数
        gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
        // 开启事件监听(JVMTI_EVENT_EXCEPTION)
        gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
        return JNI_OK;
    }
    
    JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    }
    
    

    java代码块:

    package org.itstack.demo.jvmti;
    import java.util.logging.Logger;
    
    public class TestLocationException {
    
        public static void main(String[] args) {
            Logger logger = Logger.getLogger("TestLocationException");
            try {
                User resource = new User();
                Object obj = resource.queryUserInfoById(null);
                logger.info("测试结果:" + obj);
            } catch (Exception e) {
                //屏蔽异常
            }
        }
    }
    
    class User {
        Logger logger = Logger.getLogger("User");
        public Object queryUserInfoById(String userId) {
            logger.info("根据用户Id获取用户信息" + userId);
            if (null == userId) {
                throw new NullPointerException("根据用户Id获取用户信息,空指针异常");
            }
            return userId;
        }
    }
    

    测试结果

    四月 13, 2019 12:21:45 下午 org.itstack.demo.jvmti.User queryUserInfoById
    信息: 根据用户Id获取用户信息null
    测试结果-定位类的签名:Lorg/itstack/demo/jvmti/User;
    测试结果-定位方法信息:queryUserInfoById -> (Ljava/lang/String;)Ljava/lang/Object;
    测试结果-定位方法位置:0 -> 43
    测试结果-异常类的名称:Ljava/lang/NullPointerException;
    测试结果-输出异常信息(可以分析行号):
    java.lang.NullPointerException: 根据用户Id获取用户信息,空指针异常
    	at org.itstack.demo.jvmti.User.queryUserInfoById(TestLocationException.java:23)
    	at org.itstack.demo.jvmti.TestLocationException.main(TestLocationException.java:10)
    

    其他内容:
    1、jvmti api
    2、JPDA 体系概览


    微信公众号:bugstack虫洞栈,欢迎您的关注&获取源码!

    展开全文
    Yao__Shun__Yu 2019-09-29 21:45:34
  • 1 jvmti 构成、介绍JVMTI(JVM Tool Interface) 位于jpda 最底层, 是Java 虚拟机所提供的native编程接口。 JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。2 如何使用jvmtiJVMTI是一套本地编程接口,因此...

    1 jvmti 构成、介绍

    JVMTI(JVM Tool Interface) 位于jpda 最底层, 是Java 虚拟机所提供的native编程接口。 JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。

    2 如何使用jvmti

    JVMTI是一套本地编程接口,因此使用JVMTI,需要与c/c++ 以及JNI打交道。事实上,开发时一般采用建立一个Agent的方式来使用JVMTI,Agent使用jvmti函数,设置一些回调函数,并从Java虚拟机中得到当前的运行态信息,并作出自己的判断, 最后还可能操作虚拟机的运行态。把Agent编译成一个动态链接库之后,可以再Java程序启动时来加载它(比如IDE调试时使用的libjdwp.so 就是采用这种方式),当然也可以通过Attach方式,中途加入(比如jmap, jps,jstack 等等)。

    3 Agent的加载、设置回调、卸载流程

    4 JVMTI的环境

    使用 JVMTI 的过程,主要是设置 JVMTI 环境,监听虚拟机所产生的事件,以及在某些事件上加上我们所希望的回调函数。

    可以通过操作 jvmtiCapabilities 来查询、增加、修改 JVMTI 的环境参数。

    另外,虚拟机有自己的一些功能,一开始并未被启动,那么增加或修改 jvmtiCapabilities 也是可能的,但不同的虚拟机对这个功能的处理也不太一样,多数的虚拟机允许增改,但是有一定的限制,比如仅支持在 Agent_OnLoad 时,即虚拟机启动时作出,它某种程度上反映了虚拟机本身的构架。

    5 jvmti 基本能力

    JVMTI 的功能非常丰富,包含了虚拟机中线程、内存 / 堆 / 栈,类 / 方法 / 变量,事件 / 定时器处理等等 20 多类功能。比如: 事件处理和回调函数、内存控制和对象获取、线程和锁、调试功能。具体的使用方法可以见参考文章 JVMTI 和 Agent 实现

    6 event 机制介绍

    jvmti 的event管理后续研究一下,再补上。

    7 一个agent demo

    一个agent 会经历环境初始化、参数解析、注册功能、注册事件响应这几个步骤。运行过程如下:

    e59c4eed44a2

    agent时序图

    代码如下:

    7.1 Main.cpp

    #include

    #include "MethodTraceAgent.h"

    #include "jvmti.h"

    using namespace std;

    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)

    {

    cout << "Agent_OnLoad(" << vm << ")" << endl;

    try{

    MethodTraceAgent* agent = new MethodTraceAgent();

    agent->Init(vm);

    agent->ParseOptions(options);

    agent->AddCapability();

    agent->RegisterEvent();

    } catch (AgentException& e) {

    cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";

    return JNI_ERR;

    }

    return JNI_OK;

    }

    JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)

    {

    cout << "Agent_OnUnload(" << vm << ")" << endl;

    }

    7.2 MethodTraceAgent.cpp

    #include

    #include "MethodTraceAgent.h"

    #include "jvmti.h"

    using namespace std;

    jvmtiEnv* MethodTraceAgent::m_jvmti = 0;

    char* MethodTraceAgent::m_filter = 0;

    MethodTraceAgent::~MethodTraceAgent() throw(AgentException)

    {

    // 必须释放内存,防止内存泄露

    m_jvmti->Deallocate(reinterpret_cast(m_filter));

    }

    void MethodTraceAgent::Init(JavaVM *vm) const throw(AgentException){

    jvmtiEnv *jvmti = 0;

    jint ret = (vm)->GetEnv(reinterpret_cast(&jvmti), JVMTI_VERSION_1_0);

    if (ret != JNI_OK || jvmti == 0) {

    throw AgentException(JVMTI_ERROR_INTERNAL);

    }

    m_jvmti = jvmti;

    }

    void MethodTraceAgent::ParseOptions(const char* str) const throw(AgentException)

    {

    if (str == 0)

    return;

    const size_t len = strlen(str);

    if (len == 0)

    return;

    // 必须做好内存复制工作

    jvmtiError error;

    error = m_jvmti->Allocate(len + 1,reinterpret_cast(&m_filter));

    CheckException(error);

    strcpy(m_filter, str);

    // 可以在这里进行参数解析的工作

    // ...

    }

    void MethodTraceAgent::AddCapability() const throw(AgentException)

    {

    // 创建一个新的环境

    jvmtiCapabilities caps;

    memset(&caps, 0, sizeof(caps));

    caps.can_generate_method_entry_events = 1;

    // 设置当前环境

    jvmtiError error = m_jvmti->AddCapabilities(&caps);

    CheckException(error);

    }

    void MethodTraceAgent::RegisterEvent() const throw(AgentException)

    {

    // 创建一个新的回调函数

    jvmtiEventCallbacks callbacks;

    memset(&callbacks, 0, sizeof(callbacks));

    callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;

    // 设置回调函数

    jvmtiError error;

    error = m_jvmti->SetEventCallbacks(&callbacks, static_cast(sizeof(callbacks)));

    CheckException(error);

    // 开启事件监听

    error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);

    CheckException(error);

    }

    void JNICALL MethodTraceAgent::HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method)

    {

    try {

    jvmtiError error;

    jclass clazz;

    char* name;

    char* signature;

    // 获得方法对应的类

    error = m_jvmti->GetMethodDeclaringClass(method, &clazz);

    CheckException(error);

    // 获得类的签名

    error = m_jvmti->GetClassSignature(clazz, &signature, 0);

    CheckException(error);

    // 获得方法名字

    error = m_jvmti->GetMethodName(method, &name, NULL, NULL);

    CheckException(error);

    // 根据参数过滤不必要的方法

    if(m_filter != 0){

    if (strcmp(m_filter, name) != 0)

    return;

    }

    cout << signature<< " -> " << name << "(..)"<< endl;

    // 必须释放内存,避免内存泄露

    error = m_jvmti->Deallocate(reinterpret_cast(name));

    CheckException(error);

    error = m_jvmti->Deallocate(reinterpret_cast(signature));

    CheckException(error);

    } catch (AgentException& e) {

    cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";

    }

    }

    7.3 MethodTraceAgent.h

    #include "jvmti.h"

    class AgentException

    {

    public:

    AgentException(jvmtiError err) {

    m_error = err;

    }

    char* what() const throw() {

    return "AgentException";

    }

    jvmtiError ErrCode() const throw() {

    return m_error;

    }

    private:

    jvmtiError m_error;

    };

    class MethodTraceAgent

    {

    public:

    MethodTraceAgent() throw(AgentException){}

    ~MethodTraceAgent() throw(AgentException);

    void Init(JavaVM *vm) const throw(AgentException);

    void ParseOptions(const char* str) const throw(AgentException);

    void AddCapability() const throw(AgentException);

    void RegisterEvent() const throw(AgentException);

    static void JNICALL HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method);

    private:

    static void CheckException(jvmtiError error) throw(AgentException)

    {

    // 可以根据错误类型扩展对应的异常,这里只做简单处理

    if (error != JVMTI_ERROR_NONE) {

    throw AgentException(error);

    }

    }

    static jvmtiEnv * m_jvmti;

    static char* m_filter;

    };

    7.4 MethodTraceTest.java

    public class MethodTraceTest{

    public static void main(String[] args){

    MethodTraceTest test = new MethodTraceTest();

    test.first();

    test.second();

    }

    public void first(){

    System.out.println("=> Call first()");

    }

    public void second(){

    System.out.println("=> Call second()");

    }

    }

    7.6 编译

    g++ -w -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux

    MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libagent.so

    7.7 默认运行java

    javac MethodTraceTest.java

    java MethodTraceTest

    结果如下:

    e59c4eed44a2

    默认运行

    7.8 加入我们的agent

    java -agentpath:/home/xxx/libagent.so=first MethodTraceTest” 。 //其中的first是个参数,可以透传给jvmti。

    运行结果如下:

    e59c4eed44a2

    加入agent之后的结果

    参考文献

    展开全文
    weixin_30345873 2021-02-12 23:32:32
  • 5星
    2KB mysticality 2013-06-13 17:27:11
  • weixin_34779845 2021-03-03 15:26:27
  • 13KB weixin_42685438 2021-10-04 12:04:17
  • CJQ316210 2022-01-07 15:31:02
  • 2.66MB cruby 2016-05-05 15:23:49
  • weixin_42517503 2021-03-08 16:16:11
  • cunfeng7797 2020-12-13 03:34:17
  • u013332124 2019-03-09 16:17:35
  • choupinxiao7387 2019-09-19 05:53:43
  • z1032689332 2020-02-24 14:35:32
  • weixin_39876856 2021-03-08 16:16:54
  • mcgwinds 2016-06-05 22:14:27

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,723
精华内容 1,889
关键字:

jvmti