入侵JVM?Java Agent原理浅析和实践(上)

声明:本文首发于京东零售技术公众号,为博主本人撰写投稿

前言:

在平时的开发中,我们不可避免的会使用到Debug工具,JVM作为一个单独的进程,我们使用的Debug工具可以获取JVM运行时的相关的信息,查看变量值,甚至加入断点控制,还有我们平时使用JDK自带的JMAP、JSTACK等工具,可以在JVM运行时动态的dump内存、查询线程信息,甚至一些第三方的工具,比如说京东内部使用的JEX、pfinder,阿里巴巴的Arthas,优秀的开源的框架skywalking等等,也可以做到这些,那么这些工具究竟是通过什么技术手段来实现对JVM的监控和动态修改呢?本文会进行介绍和简单的原理分析,同时附带一些样例代码来进行分析。

1.从JVMTI说起

JVM在设计之初,就考虑到了虚拟机状态的监控、debug、线程和内存分析等功能,在JDK5.0之前,JVM规范就定义了JVMPI(Java Virtual Machine Profiler Interface)也就是JVM分析接口以及JVMDI(Java Virtual Machine Debug Interface)也就是JVM调试接口,JDK5以及以后的版本,这两套接口合并成了一套,也就是Java Virtual Machine Tool Interface,就是我们这里说的JVMTI,这里需要注意的是:

  • JVMTI是一套JVM的接口规范,不同的JVM实现方式可以不同,有的JVM提供了拓展性的功能,比如openJ9,当然也可能存在JVM不提供这个接口的实现
  • JVMTI提供的是Native方式调用的API,也就是常说的JNI方式,JVMTI接口用C/C++的语言提供,最终以动态链接库的形式由JVM加载并运行

使用JNI方式调用JVMTI接口访问目标虚拟机的大体过程入下图:
在这里插入图片描述

jvmti.h头文件中定义了JVMTI接口提供的方法,但是其方法的实现是由JVM提供商实现的,比如说hotspot虚拟机其实现大部分在src\share\vm\prims\jvmtiEnv.cpp这个文件中。

2.Instrument Agent

在Jdk1.5之后,Java语言中开始提供Instrumentation接口(java.lang.instrument)让开发者可以使用Java语言编写Agent,但是其根本实现还是依靠JVMTI,只不过是SUN在工具包(sun.instrument.InstrumentationImpl)编写了一些native方法,并且然后在JDK里提供了这些native方法的实现类(jdk\src\share\instrument\JPLISAgent.c),最终需要调用jvmti.h头文件定义的方法,跟前文提到采用JNI方式访问JVMTI提供的方法并无差异,大体流程如下图:
在这里插入图片描述

但是Instrument agent仅使用到了JVMTI提供部分功能,对开发者来说,主要提供的是对JVM加载的类字节码进行插桩操作。

JVMTI方式Instrument方式
性能可以独立进程,不受目标JVM影响在目标JVM内,GC时会受到影响
功能性方法众多,功能非常全面在目标JVM内,GC时会受到影响专门提供代码“插桩”功能
易用性需要掌握C/C++,以及JNI开发相关知识Java代码开发,上手快

3 JVM启动时Agent

我们知道,JVM启动时可以指定-javaagent:xxx.jar参数来实现启动时代理,这里xxx.jar就是需要被代理到目标JVM上的JAR包,实现一个可以代理到指定JVM的JAR包需要满足以下条件:

  • JAR包的MANIFEST.MF清单文件中定义Premain-Class属性,指定一个类,加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  • JAR包中包含清单文件中定义的这个类,类中包含premain方法,方法逻辑可以自己实现

了解到这两点,我们可以定义下列类:

import java.lang.instrument.Instrumentation;

public class AgentMain {

    // JVM启动时agent
    public static void premain(String args, Instrumentation inst) {
        agent0(args, inst);
    }

    public static void agent0(String args, Instrumentation inst) {
        System.out.println("agent is running!");
        // 添加一个类转换器
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
                // JVM加载的所有类会流经这个类转换器
                // 这里找到自定义的测试类
                if (className.endsWith("WorkerMain")) {
                    System.out.println("transform class WorkerMain");
                }
                // 直接返回原本的字节码
                return classfileBuffer;
            }
        });
    }
}

JAR包内对应的清单文件(MANIFEST.MF)需要有如下内容:

PreMain-Class: AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

-javaagent 所指定 jar 包内 Premain-Class 类的 premain 方法,方法签名可以有两种:

  1. public static void premain(String agentArgs, Instrumentation inst)
  2. public static void premain(String agentArgs)

JVM会优先加载1签名的方法,加载成功忽略2,如果1没有,加载2方法。这个逻辑在sun.instrument.InstrumentationImpl类中实现。

需要说明的是,addTransformer方法的作用是添加一个字节码转换器,这个方法的入参对象需要实现ClassFileTransformer接口,唯一需要实现的方法就是transform方法,这个方法可以用来修改加载类的字节码,目前我们并不对字节码进行修改。

最后定义测试类:

package test;

import java.util.Random;

class WorkerMain {

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            int x = new Random().nextInt();
            new WorkerMain().test(x);
        }
    }

    public void test(int x) throws InterruptedException {
        Thread.sleep(2000);
        System.out.println("i'm working " + x);
    }
}

启动时添加-javaagent:xxx.jar参数,指定agent刚刚生成的JAR包,可以看到运行结果:

在这里插入图片描述

下面尝试结合JDK源码对该流程进行浅析:

JVM开始启动时会解析-javaagent参数,如果存在这个参数,就会执行Agent_OnLoad 方法读取并解析指定JAR包后生成JPLISAgent对象,然后注册jvmtiEventCallbacks.VMInit这个事件,也就是虚拟机初始化事件,并设置该事件的回调函数eventHandlerVMInit,这些代码逻辑在jdk\src\share\instrument\InvocationAdapter.cjdk\src\share\instrument\JPLISAgent.c 中实现。

在这里插入图片描述

在JVM初始化时会调用之前注册的eventHandlerVMInit事件的回调函数,进入processJavaStart这个函数,首先会在注册另一个JVM事件ClassFileLoadHook,然后会真正的执行我们在Java代码层面编写的premain方法。当JVM开始装载类字节码文件时,会触发之前注册的ClassFileLoadHook事件的回调方法eventHandlerClassFileLoadHook,这个回调函数调用transformClassFile方法,生成新的字节码,被JVM装载,完成了启动时代理的全部流程。

以上代码逻辑在jdk\src\share\instrument\JPLISAgent.c 中实现。

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值