JavaAgent技术应用和原理:JVM持久化监控

背景

字节码增强技术

  • 字节码增强:Java Agent通过修改字节码来实现对应用程序的增强,例如添加日志、性能监控、事务管理等。

  • 工具:常用的字节码增强工具包括ASM、Javassist、Byte Buddy等。

JavaAgent技术基于JVM工具接口(JVMTI),通过字节码插桩实现其功能,字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。

JavaAgent

JavaAgent技术是一种在Java虚拟机(JVM)上运行的代理程序,它允许开发者在运行时修改Java字节码,从而实现对Java应用程序的动态增强和监控。

JavaAgent定义和启动方式

Java Agent:是一个特殊的Java类,它实现了premain方法或agentmain方法;最终解耦了对代码的增强处理。

  • preMain:主程序执行前执行

  • agentMain:主程序运行后执行

一般JavaAgent技术通过两种方式启动:加载时启动和运行时启动。

  • 加载时启动的JavaAgent在类加载时进行修改,具有完全的修改权限,但修改后需要重启应用才能生效。

    • 通过在JVM启动参数中添加-javaagent:path/to/agent.jar来加载Java Agent。

    • 实现JavaAgent的premain方法:在JVM启动时调用,用于在应用程序启动前进行字节码增强。

  • 运行时启动的JavaAgent在应用运行过程中加载,可以随时对应用进行修改,但修改权限有限。

    • 通过VirtualMachine.attach(pid)方法在运行时动态加载Java Agent。

    • 实现JavaAgent的agentmain方法:在JVM运行时调用,用于在应用程序运行时进行字节码增强。

JavaAgent配置文件:MANIFEST.MF

MANIFEST.MF:Java Agent的JAR文件必须包含一个MANIFEST.MF文件,其中指定了Premain-ClassAgent-Class属性。

  • Premain-Class:指定实现premain方法的类。

  • Agent-Class:指定实现agentmain方法的类。

字节码增强工具:Instrumentation API
  • Instrumentation API:Java Agent通过Instrumentation接口来进行字节码增强。

    • addTransformer:注册一个类文件转换器,用于在类加载时修改字节码。

    • redefineClasses:重新定义已经加载的类。

    • retransformClasses:重新转换已经加载的类。

技术案例

加载时启动:JVM应用监控

  • 写一个Agent:JvmMonitorPreMainAgent
typescript 复制代码
@Slf4jpublic class JvmMonitorPreMainAgent {    public static void premain(String agentArgs, Instrumentation inst) {        log.info("this is my agent - premain:{}", agentArgs);        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {            @SneakyThrows            public void run() {                JvmStack.printMemoryInfo();                JvmStack.printGCInfo();                log.info("===================================================================================================");            }        }, 0, 5000, TimeUnit.MILLISECONDS);    }}

打印JVM信息工具类

swift 复制代码
@Slf4jpublic class JvmStack {    private static final long MB = 1048576L;    public static void printMemoryInfo() {        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();        MemoryUsage headMemory = memory.getHeapMemoryUsage();        String info = String.format("init: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",                headMemory.getInit() / MB + "MB",                headMemory.getMax() / MB + "MB", headMemory.getUsed() / MB + "MB",                headMemory.getCommitted() / MB + "MB",                headMemory.getUsed() * 100 / headMemory.getCommitted() + "%"        );        log.info("printMemoryInfo = {}", info);        MemoryUsage nonheadMemory = memory.getNonHeapMemoryUsage();        info = String.format("init: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",                nonheadMemory.getInit() / MB + "MB",                nonheadMemory.getMax() / MB + "MB", nonheadMemory.getUsed() / MB + "MB",                nonheadMemory.getCommitted() / MB + "MB",                nonheadMemory.getUsed() * 100 / nonheadMemory.getCommitted() + "%"        );        log.info("nonheadMemory = {}", info);    }    public static void printGCInfo() {        List<GarbageCollectorMXBean> garbages = ManagementFactory.getGarbageCollectorMXBeans();        for (GarbageCollectorMXBean garbage : garbages) {            String info = String.format("name: %s\t count:%s\t took:%s\t pool name:%s",                    garbage.getName(),                    garbage.getCollectionCount(),                    garbage.getCollectionTime(),                    Arrays.deepToString(garbage.getMemoryPoolNames()));            log.info("printGCInfo = {}", info);        }    }}
  • 写MAINFEST.MF
javascript 复制代码
Manifest-Version: 1.0Premain-Class: org.example.agent.JvmMonitorPreMainAgentCan-Retransform-Classes: trueCan-Redefine-Classes: trueCreated-By: Apache MavenBuilt-By: bryant
  • 配置到应用App的启动项
ruby 复制代码
-XX:+PrintGCDetails -Xmx300m -Xms100m -Xmn50m \-javaagent:/Users/bryantmo/Downloads/code/springcloud_test/agent/target/agent.jar=youCanDoIt \-XX:+HeapDumpOnOutOfMemoryError \-XX:HeapDumpPath=/Users/bryantmo/Desktop \-XX:ErrorFile=/Users/bryantmo/Desktop/error.log

-javaagent:配置了agent的jar包位置,并通过分隔符传入一个参数值"youCanDoIt"

  • 启动应用App(可以看到监控日志输出)
css 复制代码
2024-12-22 11:25:59.300  INFO [users,,,] 1607 --- [pool-1-thread-1] org.example.agent.util.JvmStack          : printMemoryInfo = init: 104MB         max: 304MB         used: 42MB         committed: 104MB         use rate: 40%2024-12-22 11:25:59.300  INFO [users,,,] 1607 --- [pool-1-thread-1] org.example.agent.util.JvmStack          : nonheadMemory = init: 7MB         max: 0MB         used: 75MB         committed: 78MB         use rate: 95%2024-12-22 11:25:59.301  INFO [users,,,] 1607 --- [pool-1-thread-1] org.example.agent.util.JvmStack          : printGCInfo = name: G1 Young Generation         count:12         took:35         pool name:[G1 Eden Space, G1 Survivor Space, G1 Old Gen]2024-12-22 11:25:59.301  INFO [users,,,] 1607 --- [pool-1-thread-1] org.example.agent.util.JvmStack          : printGCInfo = name: G1 Old Generation         count:0         took:0         pool name:[G1 Eden Space, G1 Survivor Space, G1 Old Gen]2024-12-22 11:25:59.301  INFO [users,,,] 1607 --- [pool-1-thread-1] o.example.agent.JvmMonitorPreMainAgent   : ===================================================================================================2024-12-22 11:25:59.351  INFO [users,,,] 1607 --- [           main] o.s.b.a.e.web.ServletEndpointRegistrar   : Registered '/actuator/hystrix.stream' to hystrix.stream-actuator-endpoint[5.300s][info   ][gc,start      ] GC(14) Pause Young (Normal) (G1 Evacuation Pause)[5.300s][info   ][gc,task       ] GC(14) Using 8 workers of 8 for evacuation[5.302s][info   ][gc,phases     ] GC(14)   Pre Evacuate Collection Set: 0.0ms[5.302s][info   ][gc,phases     ] GC(14)   Evacuate Collection Set: 2.1ms[5.302s][info   ][gc,phases     ] GC(14)   Post Evacuate Collection Set: 0.5ms[5.302s][info   ][gc,phases     ] GC(14)   Other: 0.1ms[5.302s][info   ][gc,heap       ] GC(14) Eden regions: 45->0(45)[5.302s][info   ][gc,heap       ] GC(14) Survivor regions: 5->5(7)[5.302s][info   ][gc,heap       ] GC(14) Old regions: 24->26[5.302s][info   ][gc,heap       ] GC(14) Archive regions: 0->0[5.302s][info   ][gc,heap       ] GC(14) Humongous regions: 0->0[5.302s][info   ][gc,metaspace  ] GC(14) Metaspace: 54544K(56064K)->54544K(56064K) NonClass: 47889K(48896K)->47889K(48896K) Class: 6654K(7168K)->6654K(7168K)[5.302s][info   ][gc            ] GC(14) Pause Young (Normal) (G1 Evacuation Pause) 73M->29M(104M) 2.795ms[5.302s][info   ][gc,cpu        ] GC(14) User=0.02s Sys=0.00s Real=0.00s[5.444s][info   ][gc,start      ] GC(15) Pause Young (Concurrent Start) (Metadata GC Threshold)[5.444s][info   ][gc,task       ] GC(15) Using 8 workers of 8 for evacuation[5.448s][info   ][gc,phases     ] GC(15)   Pre Evacuate Collection Set: 0.0ms[5.448s][info   ][gc,phases     ] GC(15)   Evacuate Collection Set: 3.0ms[5.448s][info   ][gc,phases     ] GC(15)   Post Evacuate Collection Set: 0.9ms[5.448s][info   ][gc,phases     ] GC(15)   Other: 0.1ms[5.448s][info   ][gc,heap       ] GC(15) Eden regions: 40->0(45)[5.448s][info   ][gc,heap       ] GC(15) Survivor regions: 5->5(7)[5.448s][info   ][gc,heap       ] GC(15) Old regions: 26->28[5.448s][info   ][gc,heap       ] GC(15) Archive regions: 0->0[5.448s][info   ][gc,heap       ] GC(15) Humongous regions: 0->0[5.448s][info   ][gc,metaspace  ] GC(15) Metaspace: 58802K(60160K)->58802K(60160K) NonClass: 51577K(52352K)->51577K(52352K) Class: 7225K(7808K)->7225K(7808K)[5.448s][info   ][gc            ] GC(15) Pause Young (Concurrent Start) (Metadata GC Threshold) 68M->31M(104M) 3.947ms[5.448s][info   ][gc,cpu        ] GC(15) User=0.02s Sys=0.00s Real=0.01s[5.448s][info   ][gc            ] GC(16) Concurrent Cycle[5.448s][info   ][gc,marking    ] GC(16) Concurrent Clear Claimed Marks[5.448s][info   ][gc,marking    ] GC(16) Concurrent Clear Claimed Marks 0.060ms[5.448s][info   ][gc,marking    ] GC(16) Concurrent Scan Root Regions[5.450s][info   ][gc,marking    ] GC(16) Concurrent Scan Root Regions 1.617ms[5.450s][info   ][gc,marking    ] GC(16) Concurrent Mark (5.450s)[5.450s][info   ][gc,marking    ] GC(16) Concurrent Mark From Roots[5.450s][info   ][gc,task       ] GC(16) Using 2 workers of 2 for marking[5.462s][info   ][gc,marking    ] GC(16) Concurrent Mark From Roots 11.884ms[5.462s][info   ][gc,marking    ] GC(16) Concurrent Preclean[5.462s][info   ][gc,marking    ] GC(16) Concurrent Preclean 0.064ms[5.462s][info   ][gc,marking    ] GC(16) Concurrent Mark (5.450s, 5.462s) 11.962ms[5.462s][info   ][gc,start      ] GC(16) Pause Remark[5.465s][info   ][gc,stringtable] GC(16) Cleaned string and symbol table, strings: 29316 processed, 19 removed, symbols: 182551 processed, 580 removed[5.466s][info   ][gc            ] GC(16) Pause Remark 35M->35M(104M) 3.572ms[5.466s][info   ][gc,cpu        ] GC(16) User=0.02s Sys=0.00s Real=0.00s[5.466s][info   ][gc,marking    ] GC(16) Concurrent Rebuild Remembered Sets[5.474s][info   ][gc,marking    ] GC(16) Concurrent Rebuild Remembered Sets 8.430ms[5.474s][info   ][gc,start      ] GC(16) Pause Cleanup[5.474s][info   ][gc            ] GC(16) Pause Cleanup 36M->36M(104M) 0.047ms[5.474s][info   ][gc,cpu        ] GC(16) User=0.00s Sys=0.00s Real=0.00s[5.474s][info   ][gc,marking    ] GC(16) Concurrent Cleanup for Next Mark[5.474s][info   ][gc,marking    ] GC(16) Concurrent Cleanup for Next Mark 0.094ms[5.474s][info   ][gc            ] GC(16) Concurrent Cycle 26.032ms2024-12-22 11:25:59.654  INFO [users,,,] 1607 --- [           main] org.redisson.Version                     : Redisson 3.14.0[5.587s][info   ][gc,start      ] GC(17) Pause Young (Normal) (G1 Evacuation Pause)[5.587s][info   ][gc,task       ] GC(17) Using 8 workers of 8 for evacuation[5.591s][info   ][gc,phases     ] GC(17)   Pre Evacuate Collection Set: 0.0ms[5.591s][info   ][gc,phases     ] GC(17)   Evacuate Collection Set: 3.0ms[5.591s][info   ][gc,phases     ] GC(17)   Post Evacuate Collection Set: 0.6ms[5.591s][info   ][gc,phases     ] GC(17)   Other: 0.1ms[5.591s][info   ][gc,heap       ] GC(17) Eden regions: 45->0(43)[5.591s][info   ][gc,heap       ] GC(17) Survivor regions: 5->7(7)[5.591s][info   ][gc,heap       ] GC(17) Old regions: 28->29[5.591s][info   ][gc,heap       ] GC(17) Archive regions: 0->0[5.591s][info   ][gc,heap       ] GC(17) Humongous regions: 0->0[5.591s][info   ][gc,metaspace  ] GC(17) Metaspace: 61046K(62592K)->61046K(62592K) NonClass: 53574K(54528K)->53574K(54528K) Class: 7472K(8064K)->7472K(8064K)[5.591s][info   ][gc            ] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 76M->35M(104M) 3.693ms[5.591s][info   ][gc,cpu        ] GC(17) User=0.03s Sys=0.00s Real=0.00s

运行时启动:JVM运行期的类变更

  • 写一个Agent:JvmMonitorAgentMainAgent
ruby 复制代码
@Slf4jpublic class JvmMonitorAgentMainAgent {    public static void agentmain(String agentArgs, Instrumentation inst){        log.info("this is my agent - agentmain:{}", inst);        //针对运行期的类进行增强        inst.addTransformer(new ClassTransformer(),true);        //agentmain运行时 由于堆里已经存在Class文件,所以新添加Transformer后        // 还要再调用一个  inst.retransformClasses(clazz); 方法来更新Class文件        for (Class clazz:inst.getAllLoadedClasses()) {            log.info("findout class, name = {}", clazz.getName());//            if (clazz.getName().contains("com.bryant.agent.TestAgentBean")){//                try {//                    instrumentation.retransformClasses(clazz);//                } catch (Exception e) {//                    e.printStackTrace();//                }//            }        }    }}
  • 修改MANIFEST.MF,补充Agent-Class
javascript 复制代码
Manifest-Version: 1.0Premain-Class: org.example.agent.JvmMonitorPreMainAgentAgent-Class: org.example.agent.JvmMonitorAgentMainAgentCan-Retransform-Classes: trueCan-Redefine-Classes: trueCreated-By: Apache MavenBuilt-By: bryant
  • 用JPS查看刚刚启动的应用程序APP的PID
go 复制代码
1571 EurekaApplication1492 RemoteMavenServer361606 Launcher1607 UserServer1576 ConfigServer 1498 RemoteMavenServer361631 Jps

我们的启动app是UserServer,对应的PID是1607。

  • 写一个main方法完成agent的植入
java 复制代码
public class Main {    /**     * 这个main方法可以多次被执行,在字节码层面,完成对JVM的多次热修改部署     * @param args     * @throws Exception     */    public static void main(String[] args) throws Exception {        // 获取当前 JVM 进程的 PID        String pid = "12460";//        String pid = Long.toString(ProcessHandle.current().pid());        // 加载代理        VirtualMachine vm = VirtualMachine.attach(pid);        vm.loadAgent("/Users/bryantmo/Downloads/code/springcloud_test/agent/target/agent.jar");        vm.detach();    }}
  • 执行效果是,被植入Agent的app会输出agent的操作日志(JvmMonitorAgentMainAgent会遍历每个class进行字节码增强,有需要的话可以自行补充字节码增量逻辑)
cs 复制代码
2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForByte2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForShort2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForInteger2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForLong2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForFloat2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForDouble2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForBigInteger2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveOrZeroValidatorForBigDecimal2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = javax.validation.constraints.PositiveOrZero2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.springframework.boot.system.ApplicationPid2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveValidatorForNumber2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.springframework.boot.logging.LoggingSystemProperties2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveValidatorForByte2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = org.hibernate.validator.internal.constraintvalidators.bv.number.sign.PositiveValidatorForShort2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = [Lorg.springframework.boot.ansi.AnsiColor;2024-12-22 11:30:32.620  INFO [users,,,] 1607 --- [Attach Listener] o.e.agent.JvmMonitorAgentMainAgent       : findout class, name = [Lorg.springframework.boot.ansi.AnsiElement;

应用拓展

  • JVM监控和性能分析:通过JavaAgent技术,可以在不修改源代码的情况下,对Java应用程序进行CPU、内存、线程等性能指标的监控和分析

  • 代码热替换:在运行时动态替换类定义,实现热部署和快速迭代。

  • 框架和库增强:对框架和库进行增强,如实现AOP(面向切面编程)功能,进行事务管理、安全检查等。

相关推荐
东阳马生架构11 小时前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥18 小时前
JVM系列(十三) -常用调优工具介绍
jvm
程序员志哥18 小时前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭18 小时前
聊聊volatile的实现原理?
java·jvm·redis
_LiuYan_21 小时前
JVM执行引擎JIT深度剖析
java·jvm
王佑辉1 天前
【jvm】内存泄漏的8种情况
jvm
工业甲酰苯胺1 天前
JVM简介—1.Java内存区域
java·jvm·python
yuanbenshidiaos2 天前
c++---------数据类型
java·jvm·c++
java1234_小锋2 天前
JVM对象分配内存如何保证线程安全?
jvm