javaagent技术原理

本文详细介绍了Javaagent的使用和原理,包括动态attach方式加载agent,通过字节码修改实现类的增强。通过示例代码展示了如何创建agentjar,如何使用attach工具远程加载,并解析了JVM的加载逻辑。最后,讨论了IDEA中的debug与Javaagent的关系,揭示了Javaagent在APM系统中的应用潜力。

前言

说道Javaagent是最近经常在使用这个技术,顺便了解了原理与根源,实际上就是jvm开个代理字节码修改的instrument接口。但实际上使用,根据使用的方式不同而略有区别。

1. Javaagent使用

实际上,笔者在前段时间写了arthas的启动原理(83条消息) arthas 启动原理分析_fenglllle的博客-优快云博客,简单的说明了Javaagent的2种方式,jvm参数方式与动态attach。

以动态attach为例,实际上以jvm参数的agent类似,动态attach支持远程attach。

1.1 agent jar,demo

public class AgentMainDemo {
    private static synchronized void main(String args, Instrumentation inst) {
        try {
            System.out.println("agent exec ......");
            inst.addTransformer(new ClassFileTransformer() {
                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                    //字节码修改,替换
                    System.out.println("------ byte instead -----");
                    return new byte[0];
                }
            }, true);
            Class<?> clazz = Class.forName("com.feng.agent.demo.ReTransformDemo");
            inst.retransformClasses(clazz);
        } catch (ClassNotFoundException | UnmodifiableClassException e) {
            e.printStackTrace();
        }
    }

    public static void premain(String args, Instrumentation inst) {
        main(args, inst);
    }

    public static void agentmain(String args, Instrumentation inst) {
        main(args, inst);
    }
}

pom打包manifest支持

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>com.feng.agent.demo.AgentMainDemo</Premain-Class>
                                    <Agent-Class>com.feng.agent.demo.AgentMainDemo</Agent-Class>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

 1.2 运行的Java应用&tools.jar

public class DemoMain {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("I'm a app");
        Thread.sleep(100000000000l);
    }
}

执行,可以debug执行都行。执行后pid笔者为 3041

tools.jar,需要载入才行

public class AttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine machine = null;
        try {
            machine = VirtualMachine.attach("3041");
            machine.loadAgent("/Users/huahua/IdeaProjects/java-agent-demo/attach-demo/src/main/resources/agent-demo-jar-with-dependencies.jar");
        } finally {
            if (machine != null) {
                machine.detach();
            }
        }

    }
}

 1.3 执行结果

 可以看到agent exec 的字样,说明agent已经load了,且进行了字节码替换。实际上transform的ClassFileTransformer可以形成调用链,一个类可以被多次transform。transform默认是有

ClassFileTransformer的。

2. Javaagent原理

简单介绍Javaagent的原理:Javaagent分为jvm参数方式与动态attach方式

jvm参数方式:这种方式比较常用,因为可以通过启动参数内置

动态attach:这种方式比较灵活,可以多次attach,且可以销毁attach的agent。

实际上jvm加载逻辑差不多,这里以复杂的动态attach为例

关键还是:provider.attachVirtualMachine(id); 

    public static VirtualMachine attach(String id)
        throws AttachNotSupportedException, IOException
    {
        if (id == null) {
            throw new NullPointerException("id cannot be null");
        }
        List<AttachProvider> providers = AttachProvider.providers();
        if (providers.size() == 0) {
            throw new AttachNotSupportedException("no providers installed");
        }
        AttachNotSupportedException lastExc = null;
        for (AttachProvider provider: providers) {
            try {
                return provider.attachVirtualMachine(id);
            } catch (AttachNotSupportedException x) {
                lastExc = x;
            }
        }
        throw lastExc;
    }

然后进一步跟踪:

可以看到使用了SPI技术,笔者Mac系统,如果是Linux或者win,这里是不同的

 逻辑大同小异:

static {
    System.loadLibrary("attach");
    tmpdir = getTempDir();
}

先load c的lib,然后获取临时目录

    BsdVirtualMachine(AttachProvider provider, String vmid)
        throws AttachNotSupportedException, IOException
    {
        super(provider, vmid);

        // This provider only understands pids
        int pid;
        try {
            pid = Integer.parseInt(vmid);
        } catch (NumberFormatException x) {
            throw new AttachNotSupportedException("Invalid process identifier");
        }

        //这段注释很明显,先找socket文件,找不到就创建attach文件,发送quit信号,再试查找socket文件
        // Find the socket file. 
        // If not found then we attempt to start the attach mechanism in the target VM by sending it a QUIT signal.
        // Then we attempt to find the socket file again.
        //查找socket文件
        path = findSocketFile(pid);
        if (path == null) {
            File f = new File(tmpdir, ".attach_pid" + pid);
            //创建attach文件
            createAttachFile(f.getPath());
            try {
                //发送退出信号,启动attach mechanism连接途径
                sendQuitTo(pid);

                // give the target VM time to start the attach mechanism
                int i = 0;
                long delay = 200;
                int retries = (int)(attachTimeout() / delay);
                do {
                    try {
                        Thread.sleep(delay);
                    } catch (InterruptedException x) { }
                    //多次查找socket文件
                    path = findSocketFile(pid);
                    i++;
                } while (i <= retries && path == null);
                if (path == null) {
                    throw new AttachNotSupportedException(
                        "Unable to open socket file: target process not responding " +
                        "or HotSpot VM not loaded");
                }
            } finally {
                f.delete();
            }
        }

        // Check that the file owner/permission to avoid attaching to
        // bogus process
        checkPermissions(path);

        // Check that we can connect to the process
        // - this ensures we throw the permission denied error now rather than
        // later when we attempt to enqueue a command.
        //socket创建
        int s = socket();
        try {
            //连接socket,相当于远程(另一个jvm进程)连上了pid
            connect(s, path);
        } finally {
            close(s);
        }
    }

    // Return the socket file for the given process.
    // Checks temp directory for .java_pid<pid>.
    private String findSocketFile(int pid) {
        String fn = ".java_pid" + pid;
        File f = new File(tmpdir, fn);
        return f.exists() ? f.getPath() : null;
    }

 建立socket连接,就进行下一步,loadjar

实际上这里就可以看到是要加载instrument。执行load指令,拿到结果,实际上load jar加载结束,agent就注入生效了,这个过程是JDK触发完成

    private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
        throws AgentLoadException, AgentInitializationException, IOException
    {
        InputStream in = execute("load",
                                 agentLibrary,
                                 isAbsolute ? "true" : "false",
                                 options);
        try {
            int result = readInt(in);
            if (result != 0) {
                throw new AgentInitializationException("Agent_OnAttach failed", result);
            }
        } finally {
            in.close();

        }
    }

继续load

    InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
        assert args.length <= 3;                // includes null

        // did we detach?
        String p;
        synchronized (this) {
            if (this.path == null) {
                throw new IOException("Detached from target VM");
            }
            p = this.path;
        }

        // create UNIX socket
        int s = socket();

        // connect to target VM
        try {
            connect(s, p);
        } catch (IOException x) {
            close(s);
            throw x;
        }

        IOException ioe = null;

        // connected - write request
        // <ver> <cmd> <args...>
        try {
            writeString(s, PROTOCOL_VERSION);
            writeString(s, cmd);

            for (int i=0; i<3; i++) {
                if (i < args.length && args[i] != null) {
                    //把jar的路径写给JVM,就结束了,JVM指令执行load指令
                    writeString(s, (String)args[i]);
                } else {
                    writeString(s, "");
                }
            }
        } catch (IOException x) {
            ioe = x;
        }


        // Create an input stream to read reply
        SocketInputStream sis = new SocketInputStream(s);

        // Read the command completion status
        int completionStatus;
        try {
            completionStatus = readInt(sis);
        } catch (IOException x) {
            sis.close();
            if (ioe != null) {
                throw ioe;
            } else {
                throw x;
            }
        }

        if (completionStatus != 0) {
            sis.close();

            // In the event of a protocol mismatch then the target VM
            // returns a known error so that we can throw a reasonable
            // error.
            if (completionStatus == ATTACH_ERROR_BADVERSION) {
                throw new IOException("Protocol mismatch with target VM");
            }

            // Special-case the "load" command so that the right exception is
            // thrown.
            if (cmd.equals("load")) {
                throw new AgentLoadException("Failed to load agent library");
            } else {
                throw new IOException("Command failed in target VM");
            }
        }

        // Return the input stream so that the command output can be read
        return sis;
    }

 jdk里面如何执行的呢,打开OpenJDK InvocationAdapter.c,jvm参数加载的agent执行

Agent_OnLoad函数

而动态attach的agent,执行

Agent_OnAttach函数

之所以读取manifest文件是jdk定义的,这个是动态attach,读取Agent-Class,另外还有 boot-class-path

 

 下面才是核心

 3部曲

1. 创建InstrumentationImpl实例

2. 打开ClassFileLoadHook,这个与字节码替换回调相关

3. 启动agent,实际上是调用第一步创建InstrumentationImpl实例的loadClassAndCallAgentmain方法

    private void loadClassAndCallPremain(String var1, String var2) throws Throwable {
        this.loadClassAndStartAgent(var1, "premain", var2);
    }

    private void loadClassAndCallAgentmain(String var1, String var2) throws Throwable {
        this.loadClassAndStartAgent(var1, "agentmain", var2);
    }

另一个方法就是jvm参数方式的调用函数

3. idea debug

之所以说idea的debug能力是笔者在使用jmx技术时,发现😋

是不是很有意思,-javaagent 

前面是jvmti的能力,开启debug,后面居然是一个agent,有意思

 然后查看这个premain的class

 idea转换器,关键类

CaptureInstrumentor来capture方法

 打断点的时候,类转换了

总结

实际上Javaagent类似一个sidecar的能力,不侵入应用代码即可实现字节码替换的原理,执行前检查,通过字节码修改的方式。

如果配合jmx技术,那么可以用来做APM系统非常nice,实际上pinpoint skywalking等也是使用的这种技术实现的,本次讲解了Javaagent的实现原理,现在使用就会非常明白。

### OpenTelemetry Java Agent 的工作机制与原理 #### 1. 背景介绍 OpenTelemetry 是一种用于收集和导出遥测数据的标准框架,支持分布式追踪、指标以及日志等功能[^4]。其中,OpenTelemetry Java Agent 是一个轻量级的 JVM 插件,能够在不修改应用代码的情况下自动捕获性能指标和分布式追踪数据。 #### 2. Java Agent 基础概念 Java Agent 是一种特殊的类库,在 JVM 启动时加载并允许开发者对字节码进行操作的技术。对于 VM 启动后加载的 Java Agent,`Instrumentation` 接口会通过 `agentmain` 方法传入程序。该方法在主函数 (`main`) 开始运行后才会被调用,因此适合在程序运行期间动态地替换字节码逻辑[^2]。 #### 3. 动态注入机制 为了实现动态注入功能,Java 提供了 Attach API 技术。具体来说,应用程序可以通过虚拟机提供的 `attach(pid)` 方法将代理程序连接到目标进程中,并利用 `loadAgent(AgentJarPath)` 将 Agent Jar 文件注入至对应进程。随后,目标进程会触发 `agentmain` 方法完成初始化工作。这种技术广泛应用于多种开源诊断工具中,例如 BTrace 和 Alibaba Arthas。 #### 4. OpenTelemetry Java Agent 实现细节 OpenTelemetry Java Agent 利用了上述基础能力实现了自动化监控的功能。以下是其实现的关键部分: - **字节码增强** 使用 ASM 或 Byte Buddy 等库拦截特定的方法调用并对它们附加额外的行为(如记录时间戳或上下文信息)。这种方式无需更改原始业务代码即可获取所需的数据。 - **自定义传播器接口** 针对不同协议需求,OpenTelemetry 定义了两个核心接口——`TextMapSetter` 和 `TextMapGetter` 来处理跨服务间通信中的元数据传递问题。用户可以根据实际场景自行扩展这两部分内容以适配更多平台特性[^3]。 - **集成第三方组件** 默认情况下已经内置了许多常见框架的支持模块(Spring Boot, Hibernate ORM 等),同时也鼓励社区贡献新的插件来覆盖更广泛的生态系统范围。 #### 5. 数据传输与存储 采集完毕后的跟踪事件会被序列化并通过 gRPC 协议推送到远端接收方(Collector Service)。最终由后者负责持久化保存或者进一步分析展示给运维人员查看。 ```java // 示例:如何配置 OpenTelemetry Exporter 发送数据到 Prometheus SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(new OtlpGrpcSpanExporter())) .build(); OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .buildAndRegisterGlobal(); ``` --- ####
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值