Instrumentation接口详解
一、Javaagent
Javaagent相当于一个插件,在JVM启动的时候可以添加 javaagent配置指定启动之前需要启动的agent jar包
这个agent包中需要有MANIFEST.MF文件必须指定Premain-Class配置,且Premain-Class配置指定的Class必须实现premain()方法
在JVM启动的时候,会从agent包中找到MAINIFEST.MF中配置的Class,执行其实现的premain方法,而且这一步是在main方法之前执行的。
这样就可以在JVM启动执行main方法之前做一些其他而外的操作了。
premain方法有两种
public static void premain(String agentArgs, Instrumentation inst){
//执行main方法之前的操作
} public static void premain(String agentArgs){
//执行main方法之前的操作
}
agent会优先执行第一个方法,如果第二个方法不存在则才会执行第二个方法。
javaagent使用的步骤主要如下:
1、新建agent项目,新建自定义agent的入口类,如下
1 public class MyAgent
2 {
3 /**
4 * 参数args是启动参数
5 * 参数inst是JVM启动时传入的Instrumentation实现
6 * */
7 public static void premain(String args,Instrumentation inst)
8 {
9 System.out.println("premain方法会在main方法之前执行......");11 }
12 }
2、编辑MANIFEST.MF文件,内容如下:
Mainfest-version: 1.0
Premain-class: cn.lucky.test.agent.MyAgent
3、将agent项目打包成自定义的名字,如 myagent.jar
4、在目标项目启动的时候添加JVM参数
-javaagent: myagent.jar
简单的四步就实现了一个自定义的javaagent,agent的具体实现功能就看自定义的时候如何实现premain(),可以premain方法中添加任何想要在main方法执行之前的逻辑。
premain方法中有一个参数,Instrumentation,这个是才是agent实现更强大的功能都核心所在
Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP
功能:
监控和扩展JVM上的运行程序,替换和修改java类定义,提供一套代理机制,支持独立于JVM应用程序之外的程序以代理的方式连接和访问JVM。
比如说一个Java程序在JVM上运行,这时如果需要监控JVM的状态,除了使用JDK自带的jps等命令之外,就可以通过instrument来更直观的获取JVM的运行情况;
或者一个Java方法在JVM中执行,如果我想获取这个方法的执行时间又不想改代码,常用的做法是通过Spring的AOP来实现,而AOP通过面向切面编程,实际上编译出来的类中代码也是被改动的,而instrument是在JVM层面上直接改动java方法来实现
一、Instrumentation接口源码
源码如下:
public interface Instrumentation
{
//添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer); //移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer); //是否可以被重新定义
boolean isRetransformClassesSupported(); //重新定义Class文件
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException; //是否可以修改Class文件
boolean isModifiableClass(Class<?> theClass); //获取所有加载的Class
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses(); //获取指定类加载器已经初始化的类
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader); //获取某个对象的大小
long getObjectSize(Object objectToSize); //添加指定jar包到启动类加载器检索路径
void appendToBootstrapClassLoaderSearch(JarFile jarfile); //添加指定jar包到系统类加载检索路径
void appendToSystemClassLoaderSearch(JarFile jarfile); //本地方法是否支持前缀
boolean isNativeMethodPrefixSupported(); //设置本地方法前缀,一般用于按前缀做匹配操作
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
主要是定义了操作java类的class文件方法,这里又涉及到了ClassFileTransformer接口,这个接口的作用是改变Class文件的字节码,返回新的字节码数组,源码如下:
public interface ClassFileTransformer
{ byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException;
}
ClassFileTransformer接口只有一个方法,就是改变指定类的Class文件,该接口没有默认实现,很显然如果需要改变Class文件的内容,需要改成什么样需要使用者自己来实现。
二、Instrumentation接口的使用案例
Instrumentation可以在带有main方法的应用程序之前运行,通过-javaagent参数来指定一个特点的jar文件(包含Instrumentation代理)来启动Instrumentation的代理程序,所以首先需要编写一个Instrumentation的代理程序,案例如下:
新建代理项目
public class MyAgent
{
/**
* 参数args是启动参数
* 参数inst是JVM启动时传入的Instrumentation实现
* */
public static void premain(String args,Instrumentation inst)
{
System.out.println("premain方法会在main方法之前执行......");
inst.addTransformer(new MyTransformClass());
}
} ------------------------------------------------------------------------
public class MyTransformClass implements ClassFileTransformer
{ @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException
{
// 定义重新编译之后的字符流数组
byte[] newClassFileBuffer = new byte[classfileBuffer.length];
String transClassName = "com.mrhu.opin.controller.TestController";//重定义指定类,也可以重定义指定package下的类,使用者自由发挥
if (className.equals(transClassName))
{
System.out.println("监控到目标类,重新编辑Class文件字符流...");
// TODO 对目标类的Class文件字节流进行重新编辑
// 对byte[]重新编译可以使用第三方工具如javassist,感兴趣的可自行研究
// 本文图方便,直接返回旧的字节数组
newClassFileBuffer = classfileBuffer;
}
return newClassFileBuffer;
} }
编译打包项目为 instrumentdemo.jar,然后其他在需要被监控的项目启动参数中添加如下参数:
-javaagent:instrumentdemo.jar
然后在被监控应用程序执行main方法之前就会先执行premain方法,走instrumentation代理程序,那么在应用程序加载类的时候就会进入到自定义的ClassFileTransformer中
Instrumentation还可以添加多个代理,按照代理指定的顺序依次调用
(详细案例可以自行百度了解,本文只做理论描述)
所以Instrumentation接口相当于一个代理,当执行premain方法时,通过Instrumentation提供的API可以动态的添加管理JVM加载的Class文件,Instrumentation管理着ClassFileTransformer。
ClassFileTransformer接口可以动态的改变Class文件的字节码,在加载字节码的时候可以将字节码进行动态修改,具体实现需要自定义实现类来实现ClassFileTransformer接口
那么premain方法中的Instrumentation对象是如何传入的呢?答案是JVM传入的。
三、Instrumentation的实现原理
说起Instrumentation的原理,就不得不先提起JVMTI,全程是JVM Tool Interface顾名思义是JVM提供的工具接口,也就是JVM提供给用户的扩展接口集合。
JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自行的逻辑。
比如我想监听JVM加载某个类的事件,那么我们就可以实现一个回调函数赋给jvmtiEnv的回调方法集合里的ClassFileLoadHook(Class类加载事件),那么当JVM进行类加载时就会触发回调函数,我们就可以在JVM加载类的时候做一些扩展操作,
比如上面提到的更改这个类的Class文件信息来增强这个类的方法。
JVMTI运行时,一个JVMTIAgent对应一个jvmtiEnv或者是多个,JVMTIAgent是一个动态库,利用JVMTI暴露出来的接口来进行扩展。
主要有三个函数:
Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数(通过-agentlib加载vm参数中)
Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法
Agent_OnUnload方法:在agent卸载时调用
我们常用的Eclipse等调试代码实际就是使用到了这个JVMTIAgent
回到主题,Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以再运行时加动态加载
启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式
运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载
3.1、启动时加载
Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:
1.创建并初始化JPLISAgent
2.监听VMInit事件,在vm初始化完成之后执行下面逻辑
a.创建Instrumentation接口的实例,也就是InstrumentationImpl对象
b.监听ClassFileLoadHook事件(类加载事件)
c.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
3.解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容
3.2、运行时加载
Instrument agent运行时加载会使用Agent_OnAttach方法,会通过JVM的attach机制来请求目标JVM加载对应的agent,过程如下
1.创建并初始化JPLISAgent
2.解析javaagent里的MANIFEST.MF里的参数
3.创建InstrumentationImpl对象
4.监听ClassFileLoadHook事件
5.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
3.3、ClassFileLoadHook回调实现
启动时加载和运行时加载都是监听同一个jvmti事件那就是ClassFileLoadHook,这个是类加载的事件,在读取类文件字节码之后回调用的,这样就可以对字节码进行修改操作。
在JVM加载类文件时,执行回调,加载Instrument agent,创建Instrumentation接口的实例并且执行premain方法,premain方法中注册自定义的ClassFileTransformer来对字节码文件进行操作,这个就是在加载时进行字节码增强的过程。
那么如果java类已经加载完成了,在运行的过程中需要进行字节码增强的时候还可以使用Instrumentation接口的redifineClasses方法,有兴趣的可以自行研究源码,这里只描述大致过程。
通过执行该方法,在JVM中相当于是创建了一个VM_RedifineClasses的VM_Operation,此时会stop_the_world,具体的执行过程如下:
挨个遍历要批量重定义的 jvmtiClassDefinition
然后读取新的字节码,如果有关注 ClassFileLoadHook 事件的,还会走对应的 transform 来对新的字节码再做修改
字节码解析好,创建一个 klassOop 对象
对比新老类,并要求如下:
父类是同一个
实现的接口数也要相同,并且是相同的接口
类访问符必须一致
字段数和字段名要一致
新增的方法必须是 private static/final 的
可以删除修改方法
对新类做字节码校验
合并新老类的常量池
如果老类上有断点,那都清除掉
对老类做 JIT 去优化
对新老方法匹配的方法的 jmethodId 做更新,将老的 jmethodId 更新到新的 method 上
新类的常量池的 holer 指向老的类
将新类和老类的一些属性做交换,比如常量池,methods,内部类
初始化新的 vtable 和 itable
交换 annotation 的 method、field、paramenter
遍历所有当前类的子类,修改他们的 vtable 及 itable
上面是基本的过程,总的来说就是只更新了类里的内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新所带来的开销。
另外还可以通过retransform来进行回滚操作,可以回滚到字节码之前的版本。
------------------------------------------------------------
总结:
1. Instrumentation相当于一个JVM级别的AOP
2.Instrumentation在JVM启动的时候监听事件,如类加载事件,JVM触发来指定的事件通过回调通知,并创建一个 Instrumentation接口的实例,然后找到MANIFEST.MF中配置的实现了premain方法的Class
然后将Instrumentation实例传入premain方法中
3.premain方法会在main方法之前执行,可以添加ClassFileTransfer来实现对Class文件字节码的动态修改(并不会修改Class文件中的字节码,而是修改已经被JVM加载的字节码)
4.修改字节码的技术可以使用开源的 ASM、javassist、byteBuddy等
执行premain方法是通过在JVM启动的时候实现的动态代理,那么如果想要在JVM的运行过程中实现这个功能该如何实现呢?这就需要使用JVM的attach机制
JVM提供了一种attach机制,简单点说就是可以通过一个JVM来操作、查询另一个JVM中的数据,比如最常用的jmap、jstack等命令就是通过attach机制实现的。
当需要dump一个JVM进程中的堆信息时,此时就可以通过开启另一个JVM进程,如何通过这个JVM进程来和目标JVM进程进行通信,执行想要执行的命令或者查询想要的数据
Attach 实现的根本原理就是使用了 Linux 下是文件 Socket 通信(详情可以自行百度或 Google)。有人也许会问,为什么要采用文件 socket 而不采用网络 socket?我个人认为也许一方面是为了效率(避免了网络协议的解析、数据包的封装和解封装等),另一方面是为了减少对系统资源的占用(如网络端口占用)。采用文件 socket 通信,就好比两个进程通过事先约定好的协议,对同一个文件进行读写操作,以达到信息的交互和共享。简单理解成如下图所示的模型
Instrumentation接口详解的更多相关文章
- JDBC常用接口详解
JDBC中常用接口详解 ***DriverManager 第一.注册驱动 第一种方式:DriverManager.registerDriver(new com.mysql.jdbc.Driver()) ...
- Java6.0中Comparable接口与Comparator接口详解
Java6.0中Comparable接口与Comparator接口详解 说到现在,读者应该对Comparable接口有了大概的了解,但是为什么又要有一个Comparator接口呢?难道Java的开发者 ...
- socket接口详解
1. socket概述 socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信. socket起源于UNIX,在Unix一切 ...
- “全栈2019”Java第八十四章:接口中嵌套接口详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第八十三章:内部类与接口详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- Java接口 详解(二)
上一篇Java接口 详解(一)讲到了接口的基本概念.接口的使用和接口的实际应用(标准定义).我们接着来讲. 一.接口的应用—工厂设计模式(Factory) 我们先看一个范例: package com. ...
- [转载]MII/MDIO接口详解
原文地址:MII/MDIO接口详解作者:心田麦浪 本文主要分析MII/RMII/SMII,以及GMII/RGMII/SGMII接口的信号定义,及相关知识,同时本文也对RJ-45接口进行了总结,分析了在 ...
- map接口详解
1.Map接口详解(1)映射(map)是一个存储键.键值对的对象,给定一个键,可以查询得到它的值,键和值都可以是对象(2)键必须是唯一的,值可以重复(Map接口映射唯一的键到值)(3)有些映射可以接收 ...
- ReadWriteLock 接口详解
ReadWriteLock 接口详解 这是本人阅读ReadWriteLock接口源码的注释后,写出的一篇知识分享博客 读写锁的成分是什么? 读锁 Lock readLock(); 只要没有写锁,读锁可 ...
随机推荐
- __x__(11)0906第三天__图片标签
图片标签 <img src="images/1.gif" alt="冰河世纪的大松鼠" width="80%" /> Hell ...
- BOM 浏览器对象模型_window.navigator
window.navigator 对象 包含浏览器和系统信息的 Navigator 对象. 通过这个属性 了解用户的环境信息 window.navigator.userAgent 返回浏览器的 Use ...
- Linux下Python模式下【Tab】自动补全
注:此文为转载他人博客,如有侵权,请联系我删除 1.我们需要一个tab补全的功能脚本 #!/usr/bin/python # python tab file import sys import re ...
- bitmap 合并图片
把两张bitmap覆盖合成为一张图 /** * 把两个位图覆盖合成为一个位图,以底层位图的长宽为基准 * @param backBitmap 在底部的位图 * @param frontBitmap 盖 ...
- 工厂参观记:.NET Core 中 HttpClientFactory 如何解决 HttpClient 臭名昭著的问题
在 .NET Framework 与 .NET Core 中 HttpClient 有个臭名昭著的问题,HttpClient 实现了 IDispose 接口,但当你 Dispose 它时,它不会立即关 ...
- Python函数部分(1)
之前,我们编代码时程序遵循的原则是:根据逻辑从上至下实现功能,其往往用一大段代码来实现指定功能,开发过程中最常见的操作就是粘贴复制,也就是将之前实现的代码块复制到现需功能处.这种方式虽然可以应付一般性 ...
- JAVA基础积累
1.ajax同步和异步的区别: 同步是指一个线程要等待上一个线程执行完才能开始执行,同步可以看做是一个单线程操作,只要客户端请求了,在服务器没有反馈信息之前是一个线程阻塞状态.异步是一个线程在执行中, ...
- 阿里云入坑指南&&nodejs 安装配置
买了阿里云1G1核1M的机器(800元,3年) 登录阿里云-实例-选择实例所在地区-重置密码 用公网IP ssh连接 #升级CentOS yum -y update #安装or更新组件 yum -y ...
- ReactJS antd 环境中项目上传图片后压缩(lrz的使用)
lrz说明 ( github地址 :https://github.com/think2011/localResizeIMG ) 用于:在客户端压缩好要上传的图片可以节省带宽更快的发送给后端,特别适合在 ...
- table-一列细分为多列(合并单元格)
这个是一个很常见的一个表格展示需求,其中最要的就两个属性 rowspan 和 colspan .他们分别就是合并行 与 合并列. 要做的效果是如图下面这个,联系电话就是合并了单元格.这个说法类似于ex ...