背景
字节码增强技术
字节码增强: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-Class
或Agent-Class
属性。
Premain-Class
:指定实现premain
方法的类。Agent-Class
:指定实现agentmain
方法的类。
字节码增强工具:Instrumentation API
Instrumentation API:Java Agent通过
Instrumentation
接口来进行字节码增强。addTransformer
:注册一个类文件转换器,用于在类加载时修改字节码。redefineClasses
:重新定义已经加载的类。retransformClasses
:重新转换已经加载的类。
技术案例
加载时启动:JVM应用监控
写一个Agent:JvmMonitorPreMainAgent
@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信息工具类
@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
Manifest-Version: 1.0Premain-Class: org.example.agent.JvmMonitorPreMainAgentCan-Retransform-Classes: trueCan-Redefine-Classes: trueCreated-By: Apache MavenBuilt-By: bryant
配置到应用App的启动项
-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(可以看到监控日志输出)
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
@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
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
1571 EurekaApplication1492 RemoteMavenServer361606 Launcher1607 UserServer1576 ConfigServer 1498 RemoteMavenServer361631 Jps
我们的启动app是UserServer,对应的PID是1607。
写一个main方法完成agent的植入
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进行字节码增强,有需要的话可以自行补充字节码增量逻辑)
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(面向切面编程)功能,进行事务管理、安全检查等。