Java Agent入门实战(三)-JVM Attach原理与使用

简介: Java Agent入门实战(三)-JVM Attach原理与使用

之前的permain方法只能在java程序启动之前执行,并不能程序启动之后再执行,但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了instrument的应用。而Java SE 6的新特性改变了这种情况,可以通过Java Tool API中的attach方式来达到这种程序启动之后设置代理的效果。

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

Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合VirtualMachine类完成各种功能。

结合上一篇文章和Attach,看看如何使用

Agent类增加了2个agentmain()方法,它们的参数不用,2个参数的优先级大于1个参数的,所以这里只有

agentmain (String agentArgs, Instrumentation inst)

会被执行

public class JpAgent {
    public static void premain(String agentArgs){
        System.out.println("我是一个参数的 Java Agent premain");
    }
    public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        inst.addTransformer(new JpClassFileTransformerDemo(), true);
        // retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义
        inst.retransformClasses(Dog.class);
        System.out.println("我是两个参数的 Java Agent agentmain");
    }
    public static void agentmain (String agentArgs){
        System.out.println("我是一个参数的 Java Agent agentmain");
    }
}

新增一个Dog类,内容如下,关键点在package 路径要和目标程序一样,其他无所谓

package cn.jpsite.learning;
public class Dog {
}

在之前的example01工程中新建一个带有main函数的类WhileMain.java

public class WhileMain {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(new Dog().say());
        int count = 0;
        while (true) {
            // 等待0.5秒
            Thread.sleep(500);
            count++;
            String say = new Dog().say();
            // 输出内容和次数
            System.out.println(say + count);
            // 内容不对或者次数达到1000次以上输出
            if (!"dog".equals(say) || count >= 1000) {
                System.out.println("有人偷了我的狗!");
                //break;
            }
        }
    }
}

准备一个修改过的Dog.class,内容如下,单独保存到目录D:\learning\Dog.class

// 这是一个修改后编译的.class文件,单独存放
public class Dog {
    public String say() {
        return "cat";
    }
}

resource/META-INF/MANIFEST.MF新增内容

Agent-Class: cn.jpsite.learning.javaagent01.JpAgent
Can-Retransform-Classes: true

到了这里,准备工作基本已经完成,执行打包构建,此时执行以下无论哪一条,都不会有结果agentmain输出

java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.Main
java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain
java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain

结果如下,并没用调用 agentmain 方法,这该怎么办呢?

learning>jaua-javaagentjpAgent.jar-cpexample01-1.0-SNAPSHOT.jar cn.jpsite.learning.Mair我是一个参数的 Jaua Agent premain夜太黑
-dog
D:\learning>jaua-jauaagent:jpAgent.jar-cpexample01-1.0-SNAPSHOT.jar cnjpsite.learning.WhileM我是一个参数的 Java Agent premain dog dog1 dog2 dog3 dog4
D:\learning>iava-cpexample01-1.0-SNAPSHOT.iar cn.ipsite.learning.WhileMain dog dog1
dog2  

这时候就要用到com.sun.tools.attach来帮助我们达到虚拟机启动之后的代理设置,代码如下:

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/**
 *
 * @author jiangpeng
 * @date 2019/12/1 0001
 */
public class AttachThread extends Thread {
    /**
     * 记录程序启动时的 VM 集合
     */
    private final List<VirtualMachineDescriptor> listBefore;
    /**
     要加载的agent.jar
     */
    private final String jar;
    private AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
        listBefore = vms;
        jar = attachJar;
    }
    @Override
    public void run() {
        VirtualMachine vm;
        List<VirtualMachineDescriptor> listAfter;
        int count = 0;
        try {
            while (true) {
                listAfter = VirtualMachine.list();
                vm = hasNewVm(listAfter);
                if(vm == null){
                    System.out.println("没有新jvm程序,请手动指定java pid");
                    try{
                        vm = VirtualMachine.attach("7716");
                    }catch (AttachNotSupportedException e){
                        //System.out.println("拒绝访问 Disconnected from the target VM");
                    }
                }
                Thread.sleep(1000);
                System.out.println(count++);
                if (null != vm || count >= 100) {
                    break;
                }
            }
            Objects.requireNonNull(vm).loadAgent(jar);
            vm.detach();
        } catch (Exception e) {
            System.out.println("异常:" + e);
        }
    }
    /**
     * 判断是否有新的 JVM 程序运行
     */
    private VirtualMachine hasNewVm(List<VirtualMachineDescriptor> listAfter) throws IOException, AttachNotSupportedException {
        for (VirtualMachineDescriptor vmd : listAfter) {
            if (!listBefore.contains(vmd)) {
                // 如果 VM 有增加,,我们开始监控这个 VM
                System.out.println("有新的 vm 程序:"+ vmd.displayName());
                return VirtualMachine.attach(vmd);
            }
        }
        return null;
    }
    public static void main(String[] args)  {
        new AttachThread("D:/learning/jpAgent.jar", VirtualMachine.list()).start();
    }
}

其中while循环部分每隔1秒获取一次java进程集合,如果没有的话就会提示手动指定一个java程序进行attach, 当循环了100次或者 获取到了VirtualMachine ,则退出while(true) 去加载指定的agent.jar

2种测试方法

先执行

java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain

启动example01程序,看到如下结果,还记得我们之前准备了一份Dog.class,里面的say()内容是cat的嘛,稍后你就会看到神奇的一幕

D : learning >
 D : Vlearning >
 D : learning >
 D : learning >
 D : learning 
 D : learning >
 D : vlearning >
 D : learning >
 D : Vlearning >
 D : Nlearning >
 D : Vlearning >
 D :\ learning > D : vlearning >
 D :eaHn1N9
 D :1earning> java - cp exanple01-1.D-SNAPSH0T. jar cn . jpsite . learning . WhileMain 

接下运行刚刚的AttachThread.java,看到如下内容

D:\Java\jdk1.8.0_111\bin\java. eXe
Connected to the target VM, address:'127.0.0.1:5407', transport: 'socket'
没有新jvm程序,请手动指定java pid
0
没有新jvm程序,请手动指定java pid
1
没有新jvm程序,请手动指定java pid
2
没有新jvm程序,请手动指定java pid  

使用jps -l 命令查看所有有运行中的java程序端口号

dog146
dog147
dog148
dog149
dog150
dog151
dog152
dog153
dog154
dog155
dogi56
dog157
dog158
dog159
dog160
dog161
dog162
dog163
dog164
dog165
dog166
dog167

修改AttachThread.java中的VirtualMachine.attach("7716");代码为

VirtualMachine.attach("16304");

16304为上图中WhileMain java 进程id,重新启动AttachThread.java,结果如下

loader className: cn/jpsite/learning/Dog
我是两个参数的 Java Agent agentmain

可以看到agentmain已经执行了,而且原Dog.say()方法里的内容dog也被改变了,成了cat

没有新jvm程序,请手动指定java pid
40
没有新jvm程序,请手动指定java pid
41
没有新jvm程序,请手动指定java pid
42
有新的vm 程序:cn.jpsite.learning.WhileMain
43
Disconnected from the target VM, address:'127.0.0.1:5656', transport:'socket

第二种

首先启动AttachThread.java,然后执行

java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain

,可以很快就看到结果被改变了

D:\learning>
D:\learning>
D:\learning>
D:\learning>
D:\learning>
D:\learning>
D:\learning>
D:\learning>
D:\learning>java-cpexample01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain

MANIFEST.MF的其他属性

Can-Set-Native-Method-Prefix
System-Class-Path
Boot-Class-Path

Java SE 6 新特性:BootClassPath / SystemClassPath 的动态增补

注意几点。首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中

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

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

## 资料

Java Attach API

JavaAgent源码分析

JavaAgent



目录
相关文章
|
16天前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
1月前
|
数据采集 监控 Oracle
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
阿里巴巴是 GraalVM 全球顾问委员会的唯一中国代表,阿里云程序语言与编译器团队和可观测团队合作实现了 GraalVM 应用的无侵入可观测能力,并在 ARMS 平台上线了该功能。目前在 GraalVM 24 中发布的是支持 Java agent 的第一步,其余能力将在 GraalVM 的后续版本中陆续发布。
162 21
|
1月前
|
存储 安全 Java
【Java并发】【原子类】适合初学体质的原子类入门
什么是CAS? 说到原子类,首先就要说到CAS: CAS(Compare and Swap) 是一种无锁的原子操作,用于实现多线程环境下的安全数据更新。 CAS(Compare and Swap) 的
57 15
【Java并发】【原子类】适合初学体质的原子类入门
|
29天前
|
缓存 安全 Java
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
ConcurrentHashMap是Java中线程安全的哈希表实现,支持高并发读写操作。相比Hashtable,它通过分段锁(JDK1.7)或CAS+synchronized(JDK1.8)实现更细粒度锁控制,提升性能与安全性。本文详细介绍其构造方法、添加/获取/删除元素等常用操作,并对比JDK1.7和1.8的区别,帮助开发者深入理解与使用ConcurrentHashMap。欢迎关注,了解更多!
67 4
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
|
1月前
|
Java
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
前言 有了前文对简单实用的学习 【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门 聪明的你,一定会想知道更多。哈哈哈哈哈,下面主播就...
50 6
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
|
1月前
|
安全 Java
【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门
什么是ArrayBlockingQueue ArrayBlockingQueue是 Java 并发编程中一个基于数组实现的有界阻塞队列,属于 java.util.concurrent 包,实现了 Bl...
56 6
【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门
|
1月前
|
监控 Java API
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门
前言 什么是ReentrantLock? ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 中的一个类,它实现了 Lock 接口,提供了与
76 10
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门
|
1月前
|
安全 Java
【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
前言 你是否在线程池工具类里看到过它的身影? 你是否会好奇LinkedBlockingQueue是啥呢? 没有关系,小手手点上关注,跟上主播的节奏。 什么是LinkedBlockingQueue? ...
48 1
【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
|
21天前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
56 6
|
2月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
199 29
JVM简介—1.Java内存区域