mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1309 lines
33 KiB
HTML
1309 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>33 Java Agent与字节码注入.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/00 开篇词 为什么我们要学习Java虚拟机?.md.html">00 开篇词 为什么我们要学习Java虚拟机?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的?.md.html">01 Java代码是怎么运行的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的?(上).md.html">04 JVM是如何执行方法调用的?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的?(下).md.html">05 JVM是如何执行方法调用的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的?.md.html">06 JVM是如何处理异常的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的?.md.html">07 JVM是如何实现反射的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的?(上).md.html">08 JVM是怎么实现invokedynamic的?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的?(下).md.html">09 JVM是怎么实现invokedynamic的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的?.md.html">14 Java虚拟机是怎么实现synchronized的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/16 即时编译(上).md.html">16 即时编译(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/17 即时编译(下).md.html">17 即时编译(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/18 即时编译器的中间表达形式.md.html">18 即时编译器的中间表达形式.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/19 Java字节码(基础篇).md.html">19 Java字节码(基础篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/25 循环优化.md.html">25 循环优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">26 向量化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/27 注解处理器.md.html">27 注解处理器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH(上).md.html">28 基准测试框架JMH(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH(下).md.html">29 基准测试框架JMH(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具(命令行篇).md.html">30 Java虚拟机的监控及诊断工具(命令行篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具(GUI篇).md.html">31 Java虚拟机的监控及诊断工具(GUI篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/34 Graal:用Java编译Java.md.html">34 Graal:用Java编译Java.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/35 Truffle:语言实现框架.md.html">35 Truffle:语言实现框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVM:AOT编译框架.md.html">36 SubstrateVM:AOT编译框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.html.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/工具篇 常用工具介绍.md.html">工具篇 常用工具介绍.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>33 Java Agent与字节码注入</h1>
|
||
|
||
<p>关于 Java agent,大家可能都听过大名鼎鼎的<code>premain</code>方法。顾名思义,这个方法指的就是在<code>main</code>方法之前执行的方法。</p>
|
||
|
||
<pre><code>package org.example;
|
||
|
||
|
||
|
||
public class MyAgent {
|
||
|
||
public static void premain(String args) {
|
||
|
||
System.out.println("premain");
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>我在上面这段代码中定义了一个<code>premain</code>方法。这里需要注意的是,Java 虚拟机所能识别的<code>premain</code>方法接收的是字符串类型的参数,而并非类似于<code>main</code>方法的字符串数组。</p>
|
||
|
||
<p>为了能够以 Java agent 的方式运行该<code>premain</code>方法,我们需要将其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定所谓的<code>Premain-class</code>。具体的命令如下所示:</p>
|
||
|
||
<pre><code># 注意第一条命令会向 manifest.txt 文件写入两行数据,其中包括一行空行
|
||
|
||
$ echo 'Premain-Class: org.example.MyAgent
|
||
|
||
' > manifest.txt
|
||
|
||
$ jar cvmf manifest.txt myagent.jar org/
|
||
|
||
$ java -javaagent:myagent.jar HelloWorld
|
||
|
||
premain
|
||
|
||
Hello, World
|
||
|
||
</code></pre>
|
||
|
||
<p>除了在命令行中指定 Java agent 之外,我们还可以通过 Attach API 远程加载。具体用法如下面的代码所示:</p>
|
||
|
||
<pre><code>import java.io.IOException;
|
||
|
||
|
||
|
||
import com.sun.tools.attach.*;
|
||
|
||
|
||
|
||
public class AttachTest {
|
||
|
||
public static void main(String[] args)
|
||
|
||
throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
|
||
|
||
if (args.length <= 1) {
|
||
|
||
System.out.println("Usage: java AttachTest <PID> /PATH/TO/AGENT.jar");
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
VirtualMachine vm = VirtualMachine.attach(args[0]);
|
||
|
||
vm.loadAgent(args[1]);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>使用 Attach API 远程加载的 Java agent 不会再先于<code>main</code>方法执行,这取决于另一虚拟机调用 Attach API 的时机。并且,它运行的也不再是<code>premain</code>方法,而是名为<code>agentmain</code>的方法。</p>
|
||
|
||
<pre><code>public class MyAgent {
|
||
|
||
public static void agentmain(String args) {
|
||
|
||
System.out.println("agentmain");
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>相应的,我们需要更新 jar 包中的 manifest 文件,使其包含<code>Agent-Class</code>的配置,例如<code>Agent-Class: org.example.MyAgent</code>。</p>
|
||
|
||
<pre><code>$ echo 'Agent-Class: org.example.MyAgent
|
||
|
||
' > manifest.txt
|
||
|
||
$ jar cvmf manifest.txt myagent.jar org/
|
||
|
||
$ java HelloWorld
|
||
|
||
Hello, World
|
||
|
||
$ jps
|
||
|
||
$ java AttachTest <pid> myagent.jar
|
||
|
||
agentmain
|
||
|
||
// 最后一句输出来自于运行 HelloWorld 的 Java 进程
|
||
|
||
</code></pre>
|
||
|
||
<p>Java 虚拟机并不限制 Java agent 的数量。你可以在 java 命令后附上多个<code>-javaagent</code>参数,或者远程 attach 多个 Java agent,Java 虚拟机会按照定义顺序,或者 attach 的顺序逐个执行这些 Java agent。</p>
|
||
|
||
<p>在<code>premain</code>方法或者<code>agentmain</code>方法中打印一些字符串并不出奇,我们完全可以将其中的逻辑并入<code>main</code>方法,或者其他监听端口的线程中。除此之外,Java agent 还提供了一套 instrumentation 机制,允许应用程序拦截类加载事件,并且更改该类的字节码。</p>
|
||
|
||
<p>接下来,我们来了解一下基于这一机制的字节码注入。</p>
|
||
|
||
<h2>字节码注入</h2>
|
||
|
||
<pre><code>package org.example;
|
||
|
||
|
||
|
||
import java.lang.instrument.*;
|
||
|
||
import java.security.ProtectionDomain;
|
||
|
||
|
||
|
||
public class MyAgent {
|
||
|
||
public static void premain(String args, Instrumentation instrumentation) {
|
||
|
||
instrumentation.addTransformer(new MyTransformer());
|
||
|
||
}
|
||
|
||
|
||
|
||
static class MyTransformer implements ClassFileTransformer {
|
||
|
||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||
|
||
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
|
||
|
||
System.out.printf("Loaded %s: 0x%X%X%X%X\n", className, classfileBuffer[0], classfileBuffer[1],
|
||
|
||
classfileBuffer[2], classfileBuffer[3]);
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>我们先来看一个例子。在上面这段代码中,<code>premain</code>方法多出了一个<code>Instrumentation</code>类型的参数,我们可以通过它来注册类加载事件的拦截器。该拦截器需要实现<code>ClassFileTransformer</code>接口,并重写其中的<code>transform</code>方法。</p>
|
||
|
||
<p><code>transform</code>方法将接收一个 byte 数组类型的参数,它代表的是正在被加载的类的字节码。在上面这段代码中,我将打印该数组的前四个字节,也就是 Java class 文件的魔数(magic number)0xCAFEBABE。</p>
|
||
|
||
<p><code>transform</code>方法将返回一个 byte 数组,代表更新过后的类的字节码。当方法返回之后,Java 虚拟机会使用所返回的 byte 数组,来完成接下来的类加载工作。不过,如果<code>transform</code>方法返回 null 或者抛出异常,那么 Java 虚拟机将使用原来的 byte 数组完成类加载工作。</p>
|
||
|
||
<p>基于这一类加载事件的拦截功能,我们可以实现字节码注入(bytecode instrumentation),往正在被加载的类中插入额外的字节码。</p>
|
||
|
||
<p>在工具篇中我曾经介绍过字节码工程框架 ASM 的用法。下面我将演示它的<a href="https://search.maven.org/artifact/org.ow2.asm/asm-tree/7.0-beta/jar">tree 包</a>(依赖于<a href="https://search.maven.org/artifact/org.ow2.asm/asm/7.0-beta/jar">基础包</a>),用面向对象的方式注入字节码。</p>
|
||
|
||
<pre><code>package org.example;
|
||
|
||
|
||
|
||
import java.lang.instrument.*;
|
||
|
||
import java.security.ProtectionDomain;
|
||
|
||
import org.objectweb.asm.*;
|
||
|
||
import org.objectweb.asm.tree.*;
|
||
|
||
|
||
|
||
public class MyAgent {
|
||
|
||
public static void premain(String args, Instrumentation instrumentation) {
|
||
|
||
instrumentation.addTransformer(new MyTransformer());
|
||
|
||
}
|
||
|
||
|
||
|
||
static class MyTransformer implements ClassFileTransformer, Opcodes {
|
||
|
||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||
|
||
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
|
||
|
||
ClassReader cr = new ClassReader(classfileBuffer);
|
||
|
||
ClassNode classNode = new ClassNode(ASM7);
|
||
|
||
cr.accept(classNode, ClassReader.SKIP_FRAMES);
|
||
|
||
|
||
|
||
for (MethodNode methodNode : classNode.methods) {
|
||
|
||
if ("main".equals(methodNode.name)) {
|
||
|
||
InsnList instrumentation = new InsnList();
|
||
|
||
instrumentation.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
|
||
|
||
instrumentation.add(new LdcInsnNode("Hello, Instrumentation!"));
|
||
|
||
instrumentation
|
||
|
||
.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
|
||
|
||
|
||
|
||
methodNode.instructions.insert(instrumentation);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||
|
||
classNode.accept(cw);
|
||
|
||
return cw.toByteArray();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>上面这段代码不难理解。我们将使用<code>ClassReader</code>读取所传入的 byte 数组,并将其转换成<code>ClassNode</code>。然后我们将遍历<code>ClassNode</code>中的<code>MethodNode</code>节点,也就是该类中的构造器和方法。</p>
|
||
|
||
<p>当遇到名字为<code>"main"</code>的方法时,我们会在方法的入口处注入<code>System.out.println("Hello, Instrumentation!");</code>。运行结果如下所示:</p>
|
||
|
||
<pre><code>$ java -javaagent:myagent.jar -cp .:/PATH/TO/asm-7.0-beta.jar:/PATH/TO/asm-tree-7.0-beta.jar HelloWorld
|
||
|
||
Hello, Instrumentation!
|
||
|
||
Hello, World!
|
||
|
||
</code></pre>
|
||
|
||
<p>Java agent 还提供了另外两个功能<code>redefine</code>和<code>retransform</code>。这两个功能针对的是已加载的类,并要求用户传入所要<code>redefine</code>或者<code>retransform</code>的类实例。</p>
|
||
|
||
<p>其中,<code>redefine</code>指的是舍弃原本的字节码,并替换成由用户提供的 byte 数组。该功能比较危险,一般用于修复出错了的字节码。</p>
|
||
|
||
<p><code>retransform</code>则将针对所传入的类,重新调用所有已注册的<code>ClassFileTransformer</code>的<code>transform</code>方法。它的应用场景主要有如下两个。</p>
|
||
|
||
<p>第一,在执行<code>premain</code>或者<code>agentmain</code>方法前,Java 虚拟机早已加载了不少类,而这些类的加载事件并没有被拦截,因此也没有被注入。使用<code>retransform</code>功能可以注入这些已加载但未注入的类。</p>
|
||
|
||
<p>第二,在定义了多个 Java agent,多个注入的情况下,我们可能需要移除其中的部分注入。当调用<code>Instrumentation.removeTransformer</code>去除某个注入类后,我们可以调用<code>retransform</code>功能,重新从原始 byte 数组开始进行注入。</p>
|
||
|
||
<p>Java agent 的这些功能都是通过 JVMTI agent,也就是 C agent 来实现的。JVMTI 是一个事件驱动的工具实现接口,通常,我们会在 C agent 加载后的入口方法<code>Agent_OnLoad</code>处注册各个事件的钩子(hook)方法。当 Java 虚拟机触发了这些事件时,便会调用对应的钩子方法。</p>
|
||
|
||
<pre><code>JNIEXPORT jint JNICALL
|
||
|
||
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
|
||
|
||
</code></pre>
|
||
|
||
<p>举个例子,我们可以为 JVMTI 中的<code>ClassFileLoadHook</code>事件设置钩子,从而在 C 层面拦截所有的类加载事件。关于 JVMTI 的其他事件,你可以参考该<a href="https://docs.oracle.com/en/java/javase/11/docs/specs/jvmti.html#EventIndex">链接</a>。</p>
|
||
|
||
<h2>基于字节码注入的 profiler</h2>
|
||
|
||
<p>我们可以利用字节码注入来实现代码覆盖工具(例如<a href="https://www.jacoco.org/jacoco/">JaCoCo</a>),或者各式各样的 profiler。</p>
|
||
|
||
<p>通常,我们会定义一个运行时类,并在某一程序行为的周围,注入对该运行时类中方法的调用,以表示该程序行为正要发生或者已经发生。</p>
|
||
|
||
<pre><code>package org.example;
|
||
|
||
|
||
|
||
import java.util.concurrent.ConcurrentHashMap;
|
||
|
||
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
||
|
||
|
||
public class MyProfiler {
|
||
|
||
public static ConcurrentHashMap<Class<?>, AtomicInteger> data = new ConcurrentHashMap<>();
|
||
|
||
|
||
|
||
public static void fireAllocationEvent(Class<?> klass) {
|
||
|
||
data.computeIfAbsent(klass, kls -> new AtomicInteger())
|
||
|
||
.incrementAndGet();
|
||
|
||
}
|
||
|
||
|
||
|
||
public static void dump() {
|
||
|
||
data.forEach((kls, counter) -> {
|
||
|
||
System.err.printf("%s: %d\n", kls.getName(), counter.get());
|
||
|
||
});
|
||
|
||
}
|
||
|
||
|
||
|
||
static {
|
||
|
||
Runtime.getRuntime().addShutdownHook(new Thread(MyProfiler::dump));
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>举个例子,上面这段代码便是一个运行时类。该类维护了一个<code>HashMap</code>,用来统计每个类所新建实例的数目。当程序退出时,我们将逐个打印出每个类的名字,以及其新建实例的数目。</p>
|
||
|
||
<p>在 Java agent 中,我们会截获正在加载的类,并且在每条<code>new</code>字节码之后插入对<code>fireAllocationEvent</code>方法的调用,以表示当前正在新建某个类的实例。具体的注入代码如下所示:</p>
|
||
|
||
<pre><code>package org.example;
|
||
|
||
|
||
|
||
import java.lang.instrument.*;
|
||
|
||
import java.security.ProtectionDomain;
|
||
|
||
|
||
|
||
import org.objectweb.asm.*;
|
||
|
||
import org.objectweb.asm.tree.*;
|
||
|
||
|
||
|
||
public class MyAgent {
|
||
|
||
|
||
|
||
public static void premain(String args, Instrumentation instrumentation) {
|
||
|
||
instrumentation.addTransformer(new MyTransformer());
|
||
|
||
}
|
||
|
||
|
||
|
||
static class MyTransformer implements ClassFileTransformer, Opcodes {
|
||
|
||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||
|
||
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
|
||
|
||
if (className.startsWith("java") ||
|
||
|
||
className.startsWith("javax") ||
|
||
|
||
className.startsWith("jdk") ||
|
||
|
||
className.startsWith("sun") ||
|
||
|
||
className.startsWith("com/sun") ||
|
||
|
||
className.startsWith("org/example")) {
|
||
|
||
// Skip JDK classes and profiler classes
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
|
||
ClassReader cr = new ClassReader(classfileBuffer);
|
||
|
||
ClassNode classNode = new ClassNode(ASM7);
|
||
|
||
cr.accept(classNode, ClassReader.SKIP_FRAMES);
|
||
|
||
|
||
|
||
for (MethodNode methodNode : classNode.methods) {
|
||
|
||
for (AbstractInsnNode node : methodNode.instructions.toArray()) {
|
||
|
||
if (node.getOpcode() == NEW) {
|
||
|
||
TypeInsnNode typeInsnNode = (TypeInsnNode) node;
|
||
|
||
|
||
|
||
InsnList instrumentation = new InsnList();
|
||
|
||
instrumentation.add(new LdcInsnNode(Type.getObjectType(typeInsnNode.desc)));
|
||
|
||
instrumentation.add(new MethodInsnNode(INVOKESTATIC, "org/example/MyProfiler", "fireAllocationEvent",
|
||
|
||
"(Ljava/lang/Class;)V", false));
|
||
|
||
|
||
|
||
methodNode.instructions.insert(node, instrumentation);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||
|
||
classNode.accept(cw);
|
||
|
||
return cw.toByteArray();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>你或许已经留意到,我们不得不排除对 JDK 类以及该运行时类的注入。这是因为,对这些类的注入很可能造成死循环调用,并最终抛出<code>StackOverflowException</code>异常。</p>
|
||
|
||
<p>举个例子,假设我们在<code>PrintStream.println</code>方法入口处注入<code>System.out.println("blahblah")</code>,由于<code>out</code>是<code>PrintStream</code>的实例,因此当执行注入代码时,我们又会调用<code>PrintStream.println</code>方法,从而造成死循环。</p>
|
||
|
||
<p>解决这一问题的关键在于设置一个线程私有的标识位,用以区分应用代码的上下文以及注入代码的上下文。当即将执行注入代码时,我们将根据标识位判断是否已经位于注入代码的上下文之中。如果不是,则设置标识位并正常执行注入代码;如果是,则直接返回,不再执行注入代码。</p>
|
||
|
||
<p>字节码注入的另一个技术难点则是命名空间。举个例子,不少应用程序都依赖于字节码工程库 ASM。当我们的注入逻辑依赖于 ASM 时,便有可能出现注入使用最新版本的 ASM,而应用程序使用较低版本的 ASM 的问题。</p>
|
||
|
||
<p>JDK 本身也使用了 ASM 库,如用来生成 Lambda 表达式的适配器类。JDK 的做法是重命名整个 ASM 库,为所有类的包名添加<code>jdk.internal</code>前缀。我们显然不好直接更改 ASM 的包名,因此需要借助自定义类加载器来隔离命名空间。</p>
|
||
|
||
<p>除了上述技术难点之外,基于字节码注入的工具还有另一个问题,那便是观察者效应(observer effect)对所收集的数据造成的影响。</p>
|
||
|
||
<p>举个利用字节码注入收集每个方法的运行时间的例子。假设某个方法调用了另一个方法,而这两个方法都被注入了,那么统计被调用者运行时间的注入代码所耗费的时间,将不可避免地被计入至调用者方法的运行时间之中。</p>
|
||
|
||
<p>再举一个统计新建对象数目的例子。我们知道,即时编译器中的逃逸分析可能会优化掉新建对象操作,但它不会消除相应的统计操作,比如上述例子中对<code>fireAllocationEvent</code>方法的调用。在这种情况下,我们将统计没有实际发生的新建对象操作。</p>
|
||
|
||
<p>另一种情况则是,我们所注入的对<code>fireAllocationEvent</code>方法的调用,将影响到方法内联的决策。如果该新建对象的构造器调用恰好因此没有被内联,从而造成对象逃逸。在这种情况下,原本能够被逃逸分析优化掉的新建对象操作将无法优化,我们也将统计到原本不会发生的新建对象操作。</p>
|
||
|
||
<p>总而言之,当使用字节码注入开发 profiler 时,需要辩证地看待所收集的数据。它仅能表示在被注入的情况下程序的执行状态,而非没有注入情况下的程序执行状态。</p>
|
||
|
||
<h2>面向方面编程</h2>
|
||
|
||
<p>说到字节码注入,就不得不提面向方面编程(Aspect-Oriented Programming,AOP)。面向方面编程的核心理念是定义切入点(pointcut)以及通知(advice)。程序控制流中所有匹配该切入点的连接点(joinpoint)都将执行这段通知代码。</p>
|
||
|
||
<p>举个例子,我们定义一个指代所有方法入口的切入点,并指定在该切入点执行的“打印该方法的名字”这一通知。那么每个具体的方法入口便是一个连接点。</p>
|
||
|
||
<p>面向方面编程的其中一种实现方式便是字节码注入,比如<a href="https://www.eclipse.org/aspectj/">AspectJ</a>。</p>
|
||
|
||
<p>在前面的例子中,我们也相当于使用了面向方面编程,在所有的<code>new</code>字节码之后执行了下面这样一段通知代码。</p>
|
||
|
||
<pre><code>`MyProfiler.fireAllocationEvent(<Target>.class)`
|
||
|
||
</code></pre>
|
||
|
||
<p>我曾经参与开发过一个应用了面向方面编程思想的字节码注入框架<a href="https://disl.ow2.org/">DiSL</a>。它支持用注解来定义切入点,用普通 Java 方法来定义通知。例如,在方法入口处打印所在的方法名,可以简单表示为如下代码:</p>
|
||
|
||
<pre><code>@Before(marker = BodyMarker.class)
|
||
|
||
static void onMethodEntry(MethodStaticContext msc) {
|
||
|
||
System.out.println(msc.thisMethodFullName());
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>如果有同学对这个工具感兴趣,或者有什么需求或者建议,欢迎你在留言中提出。</p>
|
||
|
||
<h2>总结与实践</h2>
|
||
|
||
<p>今天我介绍了 Java agent 以及字节码注入。</p>
|
||
|
||
<p>我们可以通过 Java agent 的类加载拦截功能,修改某个类所对应的 byte 数组,并利用这个修改过后的 byte 数组完成接下来的类加载。</p>
|
||
|
||
<p>基于字节码注入的 profiler,可以统计程序运行过程中某些行为的出现次数。如果需要收集 Java 核心类库的数据,那么我们需要小心避免无限递归调用。另外,我们还需通过自定义类加载器来解决命名空间的问题。</p>
|
||
|
||
<p>由于字节码注入会产生观察者效应,因此基于该技术的 profiler 所收集到的数据并不能反映程序的真实运行状态。它所反映的是程序在被注入的情况下的执行状态。</p>
|
||
|
||
<hr />
|
||
|
||
<p>今天的实践环节,请你思考如何注入方法出口。除了正常执行路径之外,你还需考虑异常执行路径。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/34 Graal:用Java编译Java.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a23bc513cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|