栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

入侵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虚拟机其实现大部分在srcsharevmprimsjvmtiEnv.cpp这个文件中。

2.Instrument Agent

在Jdk1.5之后,Java语言中开始提供Instrumentation接口(java.lang.instrument)让开发者可以使用Java语言编写Agent,但是其根本实现还是依靠JVMTI,只不过是SUN在工具包(sun.instrument.InstrumentationImpl)编写了一些native方法,并且然后在JDK里提供了这些native方法的实现类(jdksrcshareinstrumentJPLISAgent.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,这些代码逻辑在jdksrcshareinstrumentInvocationAdapter.c 和 jdksrcshareinstrumentJPLISAgent.c 中实现。

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

以上代码逻辑在jdksrcshareinstrumentJPLISAgent.c 中实现。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/337192.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号