【转载】Java Instrument 功能使用及原理

2023-10-27

0 介绍

利用 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了 一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地 在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的

另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成

最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。

1 基本功能和用法

JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能

事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数

1.1 VM启动前设置Instrument

Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

  1. redefineClasses 的功能比较强大,可以批量转换很多类。

640?wx_fmt=png

1.2 VM启动后动态Instrument

在 Java SE 5 当中,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性

在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序

在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

640?wx_fmt=png

同样,[1] 的优先级比 [2] 高,将会被优先执行。跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?

在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

1.3 本地方法Instrument

在 JDK 1.5 版本的 instumentation 里,并没有对Java本地方法(Native Method)的处理方式,而且在 Java 标准的 JVMTI 之下,并没有办法改变 method signature, 这就使替换本地方法非常地困难。一个比较直接而简单的想法是,在启动时替换本地代码所在的动态链接库—— 但是这样,本质上是一种静态的替换,而不是动态的 Instrumentation。而且,这样可能需要编译较大数量的动态链接库 —— 比如,我们有三个本地函数,假设每一个都需要一个替换,而在不同的应用之下,可能需要不同的组合,那么如果我们把三个函数都编译在同一个动态链接库之中,最多我们需要 8 个不同的动态链接库来满足需要。当然,我们也可以独立地编译之,那样也需要 6 个动态链接库——无论如何,这种繁琐的方式是不可接受的。

在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix

假设我们有了一个 native 函数,名字叫 nativeMethod,在运行过程中,我们需要将它指向另外一个函数(需要注意的是,在当前标准的 JVMTI 之下,除了 native 函数名,其他的 signature 需要一致)。比如我们的 Java 代码是:

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

很有趣不是吗?因此如果我们要做类似的工作,一个很好的建议是首先在 Java 中写一个带 prefix 的 native 接口,用 javah 工具生成一个 c 的 header-file,看看它实际解析得到的函数名是什么,这样我们就可以避免一些不必要的麻烦

另外一个事实是,与我们的想像不同,对于两个或者两个以上的 prefix,虚拟机并不做更多的解析;它不会试图去掉某一个 prefix,再来组装函数接口。它做且仅作两次解析

总之,新的 native 的 prefix-instrumentation 的方式,改变了以前 Java 中 native 代码无法动态改变的缺点。在当前,利用 JNI 来写 native 代码也是 Java 应用中非常重要的一个环节,因此它的动态化意味着整个 Java 都可以动态改变了 —— 现在我们的代码可以利用加上 prefix 来动态改变 native 函数的指向,正如上面所说的,如果找不到,虚拟机还会去尝试做标准的解析,这让我们拥有了动态地替换 native 代码的方式,我们可以将许多带不同 prefix 的函数编译在一个动态链接库之中,而通过 instrument 包的功能,让 native 函数和 Java 函数一样动态改变、动态替换。 当然,现在的 native 的 instrumentation 还有一些限制条件,比如,不同的 transformer 会有自己的 native prefix,就是说,每一个 transformer 会负责他所替换的所有类而不是特定类的 prefix —— 因此这个粒度可能不够精确。

1.4 BootClassPath / SystemClassPath 的动态增补

我们知道,通过设置系统参数或者通过虚拟机启动参数,我们可以设置一个虚拟机运行时的 boot class 加载路径(-Xbootclasspath)和 system class(-cp)加载路径。当然,我们在运行之后无法替换它。然而,我们也许有时候要需要把某些 jar 加载到 bootclasspath 之中,而我们无法应用上述两个方法;或者我们需要在虚拟机启动之后来加载某些 jar 进入 bootclasspath。在 Java SE 6 之中,我们可以做到这一点了。

实现这几点很简单,首先,我们依然需要确认虚拟机已经支持这个功能,然后在 premain/agantmain 之中加上需要的 classpath。我们可以在我们的 Transformer 里使用 appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch来完成这个任务

同时我们可以注意到,在 agent 的 mainfest 里加入 Boot-Class-Path 其实一样可以在动态地载入 agent 的同时加入自己的 boot class 路径,当然,在 Java code 中它可以更加动态方便和智能地完成 —— 我们可以很方便地加入判断和选择成分。

在这里我们也需要注意几点:

  1. 首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中 —— 这不是一个工程师想要得到的结果,不是吗?

  2. 其次,我们要注意到虚拟机的 ClassLoader 的工作方式,它会记录解析结果。比如,我们曾经要求读入某个类 someclass,但是失败了,ClassLoader 会记得这一点。即使我们在后面动态地加入了某一个 jar,含有这个类,ClassLoader 依然会认为我们无法解析这个类,与上次出错的相同的错误会被报告。

  3. 再次我们知道在 Java 语言中有一个系统参数“java.class.path”,这个 property 里面记录了我们当前的 classpath,但是,我们使用这两个函数,虽然真正地改变了实际的 classpath,却不会对这个 property 本身产生任何影响。

在公开的 JavaDoc 中我们可以发现一个很有意思的事情,Sun 的设计师们告诉我们,这个功能事实上依赖于 ClassLoader 的 appendtoClassPathForInstrumentation 方法 —— 这是一个非公开的函数,因此我们不建议直接(使用反射等方式)使用它,事实上,instrument 包里的这两个函数已经可以很好的解决我们的问题了

1.5 META-INF/MAINIFEST.MF清单

以下是agent jar文件的Manifest Attributes清单:

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【转载】Java Instrument 功能使用及原理 的相关文章

随机推荐

  • In order to be iterable, non-array objects must have a [Symbol.iterator]() method.

    实际使用中发现在给vue3 0 element plus增加多标签时 切换标签会发生这样的情况 查看错误猜测应该是 TagsView index vue下的moveToCurrentTag函数中tags缺少了activeClass和exac
  • chipsel语言_用VHDL语言对FPGA和CPLD器件进行开发时应注意的事项

    第25卷第4期苏 州 大 学 学 报 工 科 版 Vol 25No 4 2005年8月JOURNA L OF SOOCH OW UNIVERSIT Y ENGINEERING SCIENCE E DITION Aug 2005文章编号 16
  • CNVD-2022-03672/CNVD-2022-10270:向日葵简约版/向日葵个人版for Windows命令执行漏洞复现及修复建议

    CNVD 2022 03672 CNVD 2022 10270 向日葵简约版 向日葵个人版for Windows命令执行漏洞复现及修复建议 本文仅为验证漏洞 在本地环境测试验证 无其它目的 漏洞编号 CNVD 2022 03672 CNVD
  • Redis高可用方案-哨兵与集群

    Redis高可用方案 一 名词解释 二 主从复制 Redis主从复制模式可以将主节点的数据同步给从节点 从而保障当主节点不可达的情况下 从节点可以作为 后备顶上来 并且可以保障数据尽量不丢失 主从复制可以保障最终一致性 第二 从节点可以扩展
  • Struts2 RCE 漏洞初探

    目录 一 漏洞影响版本 二 s2 013 1 介绍 2 payload 3 请注意运用之前需要进行URL编码 如下面实例 4 获取flag 三 s2 048 1 在链接后面输入showcase 会显示出页面 2 在Gangster Name
  • Sublime text 3安装package control无响应

    一 安装package control 首先使用ctrl 快捷键或者通过View Show Console菜单打开命令行 1 1 若是sublime3 将以下代码粘贴到 代码粘贴处 然后按enter 回车 稍等片刻 import urlli
  • C语言进阶:程序预处理

    文章目录 程序预处理 程序的翻译环境 预编译 编译 汇编 链接 程序的执行环境 程序的预处理 预定义符号 define define 定义符号 注意 define 定义的宏 错误形式 define 的替换规则 注意 宏操作符 和 带副作用的
  • react里 render双层循环写法

    render循环如下数据的写法 dataArr
  • Mac下的IDEA配置

    一 JDK Java SE Development Kit 配置 参考的原文链接 https blog csdn net Tony CTO article details 128433696 JDK的下载 JDK下载路径 https www
  • JAVA随笔——关于构造函数与this关键字和static关键字

    函数重载 函数重载根据参数不同来进行分别 参数顺序不同也是函数重载 并非同一个函数 例如 public class OverloadingOrder public static void main String args f 1 z f f
  • 【Vue 入门】使用 Vue2 开发一个展示项目列表的应用

    前言 一直没有找到一个合适的展示个人项目的模板 所以自己动手使用 Vue 写了一个 该模板基于 Markdown 文件进行配置 只需要按一定规则编写 Markdown 文件 然后使用一个 在线工具 转为 JSON 文件即可 下面是该项目的在
  • 解决Qt This application failed to start because no Qt platform plugin could be initialized问题

    在出现这个问题之后 笔者按照网上的教程尝试添加了环境变量 使用windeploy部署等都没有解决问题 最终在StackOverflow终于找到了解决方法 方法一 在用户变量中添加如下内容 变量名 QT QPA PLATFORM PLUGIN
  • mysql函数实现数据脱敏,隐藏业务字符-函数的使用

    最近做了一个功能 是管理用户账户对接支付的一个模块 里面有一个是做提款账户的管理的功能 这个功能里面涉及到用户的银行卡号和手机号 根据需求为了保密性这里的银行卡号和手机号是不能在页面上展示的 需要后端来解决这个问题 因此我准备了两套方案来解
  • 被PyTorch打爆!谷歌抛弃TensorFlow,押宝JAX

    转自 新智元 很喜欢有些网友的一句话 这孩子实在不行 咱再要一个吧 谷歌还真这么干了 养了七年的TensorFlow终于还是被Meta的PyTorch干趴下了 在一定程度上 谷歌眼见不对 赶紧又要了一个 JAX 一款全新的机器学习框架 最近
  • Linux内核模块管理

    模块的全称是动态可加载内核模块 它是具有独立功能的程序 可以被单独编译 但不能独立运行 模块是为内核或其他模块提供功能的代码集合 这些模块可以是 Linux 源码中自带的 也可以是由硬件厂商开发的 可以想象成驱动 安装模块一般有两种方法 第
  • 检查处理kettle数据流中的空行

    检查处理kettle数据流中的空行 ETL处理过程中 有时需要生成数据 但是却没有输入数据 这可能有一些问题 所以通常需要ETL数据流产生一个空行数据 有时处理中需要一些聚集功能 则意味着当没有输入数据时 生成值为0 本文说明怎样检测并处理
  • 国产linux操作系统深度系统20.3发布(推荐)

    深度操作系统 deepin 是一个致力于为全球用户提供美观易用 安全稳定服务的Linux发行版 同时也一直是排名最高的来自中国团队研发的Linux发行版 了解deepin国际排名 深度操作系统20 3 升级Stable内核到5 15版本 进
  • 学习笔记-无向图的创建、深度优先遍历、广度优先遍历

    图 为什么要有图 线性表局限于一个直接前驱和一个直接后继的关系 树也只能有一个直接前驱也就是父节点 当我们需要表示一种多对多的关系就需要用到图 图是一种数据结构 其中结点可以具有零个或多个相邻元素 两个结点之间的连接称为边 结点也可以称为顶
  • 机器学习:Logistic回归介绍

    Logistic回归定义 简单来说 逻辑回归 Logistic Regression 是一种用于解决二分类 0 or 1 问题的机器学习方法 用于估计某种事物的可能性 比如某用户购买某商品的可能性 某病人患有某种疾病的可能性 以及某广告被用
  • 【转载】Java Instrument 功能使用及原理

    0 介绍 利用 java lang instrument 做动态 Instrumentation 是 Java SE 5 的新特性 它把 Java 的 instrument 功能从本地代码中解放出来 使之可以用 Java 代码的方式解决问题