Java agent实现JVM exception 统计

本文介绍了Java agent的原理和实现,包括JVMTI、JVMTIAgent和javaagent的概念。讲解了如何在类加载前后进行字节码拦截修改,以及如何在运行时动态加载agent。通过示例说明了如何编译和attach Java agent,以实现JVM异常的统计功能。

参考文献
http://www.infoq.com/cn/articles/javaagent-illustrated
http://lovestblog.cn/blog/2014/06/18/jvm-attach/

原理

对于javaagent,或许大家都听过,甚至使用过,常见的用法大致如下:

java -javaagent:myagent.jar=mode=test Test

我们通过-javaagent来指定我们编写的agent的jar路径(./myagent.jar),以及要传给agent的参数(mode=test),在启动的时候这个agent就可以做一些我们希望的事了。

javaagent的主要功能如下:

  • 可以在加载class文件之前做拦截,对字节码做修改
  • 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
    还有其他一些小众的功能
  • 获取所有已经加载过的类
  • 获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)
  • 获取某个对象的大小
  • 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
  • 将某个jar加入到classpath里供AppClassloard去加载
  • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
    想象一下可以让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。

JVMTI

JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。

比如最常见的,我们想在某个类的字节码文件读取之后、类定义之前修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那就可以实现一个回调函数赋给jvmtiEnv(JVMTI的运行时,通常一个JVMTIAgent对应一个jvmtiEnv,但是也可以对应多个)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数中,大致实现如下:,

jvmtiEventCallbacks callbacks;

jvmtiEnv *          jvmtienv = jvmti(agent);

jvmtiError          jvmtierror;

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

callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;

jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,

                                             &callbacks,

                                             sizeof(callbacks));

JVMTIAgent

JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

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

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

JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);

  • Agent_OnLoad函数,如果agent是在启动时加载的,也就是在vm参数里通过-agentlib来指定的,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。
  • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数。
  • Agent_OnUnload函数,在agent卸载时调用,不过貌似基本上很少实现它。
    其实我们每天都在和JVMTIAgent打交道,只是你可能没有意识到而已,比如我们经常使用Eclipse等工具调试Java代码,其实就是利用JRE自带的jdwp agent实现的,只是IDEA等工具在没让你察觉的情况下将相关参数(类似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自动加到程序启动参数列表里了,其中agentlib参数就用来跟要加载的agent的名字,比如这里的jdwp(不过这不是动态库的名字,JVM会做一些名称上的扩展,比如在Linux下会去找libjdwp.so的动态库进行加载,也就是在名字的基础上加前缀lib,再加后缀.so),接下来会跟一堆相关的参数,将这些参数传给Agent_OnLoad或者Agent_OnAttach函数里对应的options。

javaagent

说到javaagent,必须要讲的是一个叫做instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),因为javaagent功能就是它来实现的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。

instrument agent

instrument agent实现了Agent_OnLoadAgent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:myagent.jar的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制(JVM Attach机制实现),通过发送load命令来加载agent。

instrument agent的核心数据结构如下:

struct _JPLISAgent {
    JavaVM *                mJVM;                   /* handle to the JVM */
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */
    char const *            mAgentClassName;        /* agent class name */
    char const *            mOptionsString;         /* -javaagent options string */
};

struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};

这里解释一下几个重要项:

  • mNormalEnvironment:主要提供正常的类transform及redefine功能。
  • mRetransformEnvironment:主要提供类retransform功能。
  • mInstrumentationImpl:这个对象非常重要,也是我们Java agent和JVM进行交互的入口,或许写过javaagent的人在写premain以及agentmain方法的时候注意到了有个Instrumentation参数,该参数其实就是这里的对象。
  • mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在启动时加载的,则该方法会被调用。
  • mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,该方法在通过attach的方式动态加载agent的时候调用。
  • mTransform:指向sun.instrument.InstrumentationImpl.transform方法。
    mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class
  • mOptionsString:传给agent的一些参数。
  • mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置Can-Redefine-Classes:true
  • mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true
  • mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,将会设置mRetransformEnvironment的mIsRetransformer为true。
    在启动时加载instrument agent

正如前面“概述”里提到的方式,就是启动时加载instrument agent,具体过程都在InvocationAdapter.cAgent_OnLoad方法里,这里简单描述下过程:

创建并初始化JPLISAgent

监听VMInit事件,在vm初始化完成之后做下面的事情:
1. 创建InstrumentationImpl对象
2. 监听ClassFileLoadHook事件
3. 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
4. 解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的一些内容
5. 在运行时加载instrument agent

在运行时加载的方式,大致按照下面的方式来操作:

VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);

上面会通过JVM的attach机制来请求目标JVM加载对应的agent,过程大致如下:

创建并初始化JPLISAgent
解析javaagent里MANIFEST.MF里的参数
创建InstrumentationImpl对象
监听ClassFileLoadHook事件
调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Agent-Class类的agentmain方法

实现

#include <jni.h>
#include <jvmti.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

_Atomic(int32_t) count = ATOMIC_VAR_INIT(0);


JNIEXPORT jint JNICALL
  Java_com_github_marschall_excount_ExceptionCounter_getCount(JNIEnv *env,
                                                              jobject thisObj) {
    return atomic_load(&count);
}

JNIEXPORT jint JNICALL
  Java_com_github_marschall_excount_ExceptionCounter_clearAndGetCount(JNIEnv *env,
                                                                      jobject thisObj) {
    return atomic_exchange(&count, 0);
}


void JNICALL ExceptionCallback(jvmtiEnv *jvmti, JNIEnv *env, jthread thread,
                               jmethodID method, jlocation location, jobject exception,
                               jmethodID catch_method, jlocation catch_location) {
    atomic_fetch_add(&count , 1);
    char* class_name;
        jclass exception_class = (*env)->GetObjectClass(env, exception);
        (*jvmti)->GetClassSignature(jvmti, exception_class, &class_name, NULL);
        fprintf(stdout, "Exception: %s\n", class_name);
}


JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv* jvmti;
    jvmtiEventCallbacks callbacks;
    jvmtiCapabilities capabilities;

    if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
        fprintf(stderr, "GetEnv failed\n");
        return -1;
    }

    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_exception_events = 1;
    if ((*jvmti)->AddCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "AddCapabilities failed\n");
        return -1;
    }

    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.Exception = ExceptionCallback;
    if ((*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks)) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "SetEventCallbacks failed\n");
        return -1;
    }
    if ((*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "SetEventNotificationMode failed\n");
        return -1;
    }
    return 0;
}

/* JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved) */
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    jvmtiEnv* jvmti;
    jvmtiCapabilities capabilities;

    if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
        fprintf(stderr, "GetEnv failed\n");
        return;
    }

    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_exception_events = 1;
    if ((*jvmti)->RelinquishCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "RelinquishCapabilities failed\n");
    }
}

编译

$JAVA_HOME/include/linux -Wall -fPIC excount.c -o excount.so

attach

java -cp $JAVA_HOME/lib/tools.jar:xxx/ec.jar xxx.ExceptionCounterAttacher -p pidToAttach -a xxx/excount.so

### 通过 Java Agent 修改 JVM 的运行时间而不影响系统时间 Java Agent 是一种可以在运行时动态修改字节码的技术,允许开发者在不改变程序源代码的情况下,对类的字节码进行增强或修改。结合 Java Agent 和 `java.lang.instrument` 包的功能,可以实现修改 JVM 的时间设置而不影响系统时间。 #### 使用 Java Agent 动态修改 JVM 时间 Java Agent 可以通过以下方式实现动态修改 JVM 的时间设置: 1. **创建一个 Java Agent** 创建一个包含 `premain` 方法的 Java Agent,用于在 JVM 启动时加载并修改相关行为。 ```java import java.lang.instrument.Instrumentation; public class TimeAgent { public static void premain(String agentArgs, Instrumentation inst) { System.setProperty("user.timezone", "GMT+8"); // 设置默认时区为东八区[^1] System.out.println("TimeAgent: Set timezone to GMT+8"); } } ``` 2. **动态修改 JVM 时间** 如果需要在程序运行过程中动态修改时间,可以通过 `com.sun.tools.attach.VirtualMachine` 类来附加到正在运行的 JVM,并加载 Agent[^3]。 ```java import com.sun.tools.attach.VirtualMachine; public class AttachAgent { public static void main(String[] args) throws Exception { String pid = "12345"; // 目标 JVM 的进程 ID VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("path/to/TimeAgent.jar"); // 加载 Agent JAR 文件 vm.detach(); } } ``` 3. **使用 JVMTI 修改时间相关的字节码** 如果需要更深入地修改 JVM 的时间行为,可以借助 JVMTI(Java Virtual Machine Tool Interface)来拦截和修改与时间相关的字节码。例如,拦截 `System.currentTimeMillis()` 方法并返回自定义的时间值[^2]。 ```c JNIEXPORT void JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { jint result; JNIEnv *env; result = vm->GetEnv((void **)&env, JNI_VERSION_1_6); if (result != JNI_OK) { return; } jclass systemClass = env->FindClass("java/lang/System"); jmethodID currentTimeMillisMethod = env->GetStaticMethodID(systemClass, "currentTimeMillis", "()J"); // 替换 currentTimeMillis 方法 env->DefineClass(...); // 定义新的字节码 } ``` #### 注意事项 - 使用 Java Agent 修改 JVM 时间可能会影响所有依赖于时间的逻辑,因此需要谨慎设计和测试。 - 动态加载 Agent 的方法依赖于 `com.sun.tools.attach` 包,该包并非 JVM 标准规范的一部分,可能在某些环境中不可用。 - 修改时间相关的字节码可能会导致不可预测的行为,建议仅在受控环境中使用。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值