jvm指令介绍及在线debug工具原理
Java Virtual Machine:
To understand the details of the bytecode, we need to discuss how a Java Virtual Machine (JVM) works regarding the execution of the bytecode. JVM is a platform-independent execution environment that converts Java bytecode into machine language and executes it. A JVM is a stack-based machine. Each thread has a JVM stack which stores frames. A frame is created each time a method is invoked, and consists of an operand stack, an array of local variables, and a reference to the runtime constant pool of the class of the current method.
Stack Based Virtual Machines:
We need to know a little about stack based VM to better understand Java Bytecode. A stack based virtual machine the memory structure where the operands are stored is a stack data structure. Operations are carried out by popping data from the stack, processing them and pushing in back the results in LIFO (Last in First Out) fashion. In a stack based virtual machine, the operation of adding two numbers would usually be carried out in the following manner (where 20, 7, and “result” are the operands):
jvm指令list说明,https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
查看字节码的两种方式 idea- view- show bytecode(安装方式Preferences- Plugins- 搜“Bytecode Viewer”)![image.png | center | 1052x334 image.png | center | 1052x334](https://private-alipayobjects.alipay.com/alipay-rmsdeploy-image/skylark/png/0d52d517-9d86-40e3-bec7-8ef7552fed36.png)
ClassReader reader = new ClassReader(Hello.class.getName());
reader.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(System.out)), ClassReader.SKIP_DEBUG);
静态方法与非静态方法静态方法与非静态方法的调用核心区别在于,本地变量数组的第一个元素是否是this,比如hello、hi两个空方法
public class Hello { public void hello() { public static void hi() { public class com/aliyun/demo/app/Hello { public hello()V LINENUMBER 9 L0 RETURN //非静态方法本地变量的第一个值是“this” //本地变量 名称 类型 作用范围 index LOCALVARIABLE this Lcom/aliyun/demo/app/Hello; L0 L1 0 MAXSTACK = 0 MAXLOCALS = 1 public static hi()V LINENUMBER 12 L0 RETURN MAXSTACK = 0 MAXLOCALS = 0 }operand stack vs local variables
初步了解操作栈、本地变量数组的“变化过程”
public int hello(String name, String desc) { int i = 100; int j = 200; return i + j; public hello(Ljava/lang/String;Ljava/lang/String;)I LINENUMBER 9 L0 //向栈写入100 BIPUSH 100 //operand stack[100],local variables[this,name,desc] //把栈顶的元素存储到本地数组的index=3的位置 ISTORE 3 //operand stack[],local variables[this,name,desc,100] LINENUMBER 10 L1 //向栈写入200 SIPUSH 200 //operand stack[200],local variables[this,name,desc,100] //存储栈顶的元素到本地数组的index=4的位置 ISTORE 4 //operand stack[],local variables[this,name,desc,100,400] LINENUMBER 11 L2 //把本地数组index=3的元素加载的操作栈中 ILOAD 3 //operand stack[100],local variables[this,name,desc,100,200] //把本地数组index=4的元素加载的操作栈中 ILOAD 4 //operand stack[100,200],local variables[this,name,desc,100,200] //把栈顶的两个元素相加后,结果写入栈中 IADD //operand stack[300],local variables[this,name,desc,100,200] IRETURN //返回的值在operand stack的顶 LOCALVARIABLE this Lcom/aliyun/demo/app/Hello; L0 L3 0 LOCALVARIABLE name Ljava/lang/String; L0 L3 1 LOCALVARIABLE desc Ljava/lang/String; L0 L3 2 LOCALVARIABLE i I L1 L3 3 LOCALVARIABLE j I L2 L3 4 MAXSTACK = 2 //operand stack的max(length)=2 MAXLOCALS = 5 //本地存储有5个值 }初始的local variables中的值分别为可选的this对象(静态方法没有)及方法参数,故可通过在方法开始时读取local variables获取方法的入参、修改入参等 在方法结束时返回值位于栈顶,可以读取“operand stack”获取方法的返回值 try catch异常
有人听过try catch会比较耗时吗?通过字节码分析一下原因
public void hello(double i) { try { System.out.println(i); } catch (RuntimeException e) { throw e; public hello(D)V TRYCATCHBLOCK L0 L1 L2 java/lang/RuntimeException//try开始 LINENUMBER 10 L0 //获取System.out类型为PrintStream的对象并写入栈中 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;//operand stack[out]、local variables[this,i] //加载本地变量index=1的值到栈中 DLOAD 1 //operand stack[out,i]、local variables[this,i] //调用out的虚函数println,会消耗栈中的out、i,所以调用完栈空了(如果有返回值会写入栈中) INVOKEVIRTUAL java/io/PrintStream.println (D)V //operand stack[]、local variables[this,i] LINENUMBER 13 L1 GOTO L3//没有异常跳到L3,即catch结束的位置 L2//catch开始 LINENUMBER 11 L2 //新的frame,如果没有异常,那么就不会开新的frame FRAME SAME1 java/lang/RuntimeException//F_SAME1 representing frame with exactly the same locals as the previous frame and with single value on the stack ( nStack is 1 and stack[0] contains value for the type of the stack item). //before astore- operand stack[exception],local variables[this,i] ASTORE 3 //operand stack[],local variables[this,i,,exception] LINENUMBER 12 L4 ALOAD 3 //operand stack[exception],local variables[this,i,,exception] ATHROW LINENUMBER 14 L3 FRAME SAME//Represents a compressed frame with exactly the same locals as the previous frame and with an empty stack. //operand stack[],local variables[this,i,,exception] LOCALVARIABLE e Ljava/lang/RuntimeException; L4 L3 3 LOCALVARIABLE this Lcom/aliyun/demo/app/Hello; L0 L5 0 LOCALVARIABLE i D L0 L5 1 MAXSTACK = 3 MAXLOCALS = 4 }
总结:通过TRYCATCHBLOCK、GOTO、LABEL方式可以方法增加try catch,也可以把异常吃掉等
问题:本例中local variables中index为2的没有被使用,why?
cglib与javassist是高级的字节码工具,asm与bcel是低级的字节码工具(支持java字节码指令),低级的工具会更灵活,故选择asm来作为分析,asm文档参见
http://asm.ow2.org/doc/developer-guide.html,http://download.forge.objectweb.org/asm/asm4-guide.pdf
如何把增强的代码加入到当前的jvm中?spring已经给了我们答案,但是有两个疑问:
HelloService$EnhancerByCGLIB$41503a7e,为什么不是HelloService ?答:HelloService已经被AppClassLoader加载(通常情况下),不能重复加载相同名称的class
bean的实例的getSuperClass()的值为什么是HelloService ?答:HelloService helloService=(HelloService)getBean(“helloService”),如果动态生成的class不是被增强的class子类,强制转化报异常
AppClassLoader并未暴露defineClass方法,如何加载的增强类?答:通过反射调用classloader.defineClass即可
故spring本质是对代理类做了一个子类,故除了对“代理类字节码”增强外,还需要修改“代理类字节码”的parent、及class name
asm简化版本的spring模式字节码增强 DemoClassVisitor:对方法的增强,入参、返回值、异常、耗时 ChangeParentClassVisitor:修改类的parent ChangeNameClassVisitor:修改类名称注:asm代码主要使用了visitor、责任链模式,理解了这两个设计模式代码读起来比较容易
//入参分别为HelloService,HelloServiceQyf,返回增强后的HelloServiceQyf的字节码 private static byte[] transform(String oldName, String newName) throws IOException { ClassReader cr = new ClassReader(oldName); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); final String oldTypeName = oldName.replace(".", "/"); final String newTypeName = newName.replace(".", "/"); ClassVisitor visitor = null; //对方法增强 visitor = new DemoClassVisitor(cw, DemoAdvice.class); //修改parent visitor = new ChangeParentClassVisitor(visitor, oldTypeName); //增加rename逻辑 visitor = new ChangeNameClassVisitor(visitor, oldTypeName, newTypeName); cr.accept(visitor, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); }DemoClassVisitor详解
ChangeParentClassVisitor、ChangeNameClassVisitor不涉及jvm指令代码相对简单,故我们重点介绍DemoClassVisitor,它通过继承asm的工具类AdviceAdapter,分别实现onMethodEnter、onMethodExit,核心代码如下:
@Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.contains("")) { return mv; return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) { private final Label tryCatchStart = new Label(), tryCatchEnd = new Label(), exceptionHandler = new Label(); @Override public void onMethodEnter() { {//try start visitTryCatchBlock(tryCatchStart, tryCatchEnd, exceptionHandler, "java/lang/Exception"); visitLabel(tryCatchStart); {//方法开始植入计数起始 visitLdcInsn(name); visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "start", "(Ljava/lang/String;)V", false); {//获取方法入参值 Type types[] = Type.getArgumentTypes(desc); if (types.length == 0) { return; for (int i = 0; i types.length; i++) { visitLdcInsn(name); visitLdcInsn(i); loadArg(i); visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "args", "(Ljava/lang/String;ILjava/lang/Object;)V", false); @Override public void onMethodExit(int opcode) { if (opcode != RETURN) {//如果有返回值,提取出来 int returnIndex = newLocal(Type.getReturnType(desc)); dup(); storeLocal(returnIndex); visitLdcInsn(name); loadLocal(returnIndex); visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "result", "(Ljava/lang/String;Ljava/lang/Object;)V", false); {//方法调用结束,打印方法名,入参 visitLdcInsn(name); visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "commit", "(Ljava/lang/String;)V", false); {//try end visitLabel(tryCatchEnd); Label exitMethodLabel = new Label(); visitJumpInsn(GOTO, exitMethodLabel); visitLabel(exceptionHandler);//operand stack[exception] dup();//operand stack[exception,exception] visitLdcInsn(name);//operand stack[exception,exception,name] swap();//operand stack[exception,name,exception] visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "exception", "(Ljava/lang/String;Ljava/lang/Object;)V", false); //operand stack[exception] mv.visitInsn(Opcodes.ATHROW); visitLabel(exitMethodLabel); }获取方法的入参代码
onMethodEnter中增加如下code:
Type types[] = Type.getArgumentTypes(desc);//desc=(D)V,double入参,无返回值 if (types.length == 0) { return; for (int i = 0; i types.length; i++) {//参数迭代 //向操作栈写入name visitLdcInsn(name);//operand stack[name] //向操作栈写入参数的index visitLdcInsn(i);//operand stack[name,i] //从本地数组加载方法的index对应的入参 loadArg(i);//operand stack[name,i,arg_i] //调用args(String method, int index, Object object),消耗掉name、i、arg_i visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "args", "(Ljava/lang/String;ILjava/lang/Object;)V", false); }获取方法的异常
onMethodEnter中最上面增加如下code:
visitTryCatchBlock(tryCatchStart, tryCatchEnd, exceptionHandler, "java/lang/Exception"); visitLabel(tryCatchStart);
onMethodExit中最下面增加如下code:
visitLabel(tryCatchEnd);//try end Label exitMethodLabel = new Label(); visitJumpInsn(GOTO, exitMethodLabel);//方法没有异常直接到exitMethodLabel位置 visitLabel(exceptionHandler);//catch start,operand stack[exception] dup();//复制operand stack一个top元素,operand stack[exception,exception] //operand stack增加方法名 visitLdcInsn(name);//operand stack[exception,exception,name] //交互operand stack顶的两个元素位置 swap();//operand stack[exception,name,exception] //调用静态方法exception(String method, Object object) visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "exception", "(Ljava/lang/String;Ljava/lang/Object;)V", false); //operand stack[exception] mv.visitInsn(Opcodes.ATHROW); visitLabel(exitMethodLabel);获取方法的返回值
onMethodExit中增加如下code:
if (opcode != RETURN) {//RETURN表示方法为void,故不需要抓取返回值 int returnIndex = newLocal(Type.getReturnType(desc));//operand stack[result] //不能把原来的result用了,需要复制一份 dup();//operand stack[result,result] storeLocal(returnIndex);//operand stack[result] visitLdcInsn(name);//operand stack[result,name] loadLocal(returnIndex);//operand stack[result,name,result] //调用result(String method, Object object) visitMethodInsn(Opcodes.INVOKESTATIC, adviceName, "result", "(Ljava/lang/String;Ljava/lang/Object;)V", false); //operand stack[result] }
原始的HelloService代码
public class HelloService { public void sayHi(String message) { System.out.println("--------------sayHi start---------------"); System.out.println(this.getClass().getName() + ".sahHi:" + message); System.out.println("--------------sayHi end------------------"); public void exception() { int i = 1 / 0; }
@Test public void sayHi() throws Exception { HelloService hello = ApplicationContext.getBean(HelloService.class.getName()); hello.sayHi("hi"); 控制台输出 --------------sayHi start--------------- com.aliyun.demo.app.HelloServiceYqf.sahHi:hi --------------sayHi end------------------ --------------asm 字节码增强信息 start----------- 调用方法:sayHi 方法入参:hi 响应耗时:6 ms --------------asm 字节码增强信息 end-----------注意虽然调用的是HelloService,但实际运行是HelloServiceYqf 通过增强,我们获取导入入参、响应时间(该方法为void故无返回值)
@Test public void exception() { try { HelloService hello = ApplicationContext.getBean(HelloService.class.getName()); hello.exception(); } catch (Exception e) { System.out.println("main- " + e.toString()); 控制台输出,主线程(main- )与增强都获取了异常信息 --------------asm 字节码增强信息 start----------- 调用方法:exception 方法入参:无 异常信息:java.lang.ArithmeticException: / by zero 响应耗时:0 ms --------------asm 字节码增强信息 end----------- main- java.lang.ArithmeticException: / by zero吃掉异常流程
注释掉onMethodExit中异常代码的dup及mv.visitInsn(Opcodes.ATHROW)代码即可,被拦截的方法必须没有返回值,否则需要在代码里生成默认值
控制台输出
--------------asm 字节码增强信息 start----------- 调用方法:exception 方法入参:无 异常信息:java.lang.ArithmeticException: / by zero 响应耗时:0 ms --------------asm 字节码增强信息 end-----------注意main开头的打印代码未出现在控制台 instrument
Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。
在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
更为详细的参见https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
偷懒把依赖的jar包也打进去,方便一些
plugin groupId org.apache.maven.plugins /groupId artifactId maven-assembly-plugin /artifactId executions execution goals goal attached /goal /goals phase package /phase configuration descriptorRefs descriptorRef jar-with-dependencies /descriptorRef /descriptorRefs archive manifestEntries Premain-Class com.aliyun.demo.app.AgentLauncher /Premain-Class Agent-Class com.aliyun.demo.app.AgentLauncher /Agent-Class Can-Redefine-Classes true /Can-Redefine-Classes Can-Retransform-Classes true /Can-Retransform-Classes /manifestEntries /archive /configuration /execution /executions /pluginagent启动类
代码只用到了DemoClassVisitor,并不需要替换名称、及父类,因为Java Tool API提供了“直接修改已加载的class字节码”的能力,和“类spring”方式不同
//mvn clean package -Dmaven.test.skip=true,打包 public class AgentLauncher { public static void agentmain(String args, Instrumentation inst) { final String regex = StringUtils.isEmpty(args) ? "com.aliyun.demo.+Service" : args; System.out.println("filter:" + regex); inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) - { if (!Pattern.matches(regex, className)) { return null; System.out.println("transform class:" + className); ClassReader reader = new ClassReader(classfileBuffer); ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); DemoClassVisitor visitor = new DemoClassVisitor(writer, DemoAdvice.class); reader.accept(visitor, ClassReader.EXPAND_FRAMES); return writer.toByteArray(); }, true); for (Class cls : inst.getAllLoadedClasses()) { try { if (Pattern.matches(regex, cls.getName())) { inst.retransformClasses(cls); } catch (UnmodifiableClassException e) { e.printStackTrace(); }attach目标jvm(windows无效)
VirtualMachine及VirtualMachineDescriptor是非标准包,在windows下是没有的!
@Test public void agentTest() throws Exception { //jarName需要修改,改成自己的打包生成路径 String jarName = "/Users/ghost/works/hello/target/hello-1.0-SNAPSHOT-jar-with-dependencies.jar"; String targetClass = "HelloTest"; //需要增强的package正则 String filter = "com.aliyun.demo.+Service"; for (VirtualMachineDescriptor vd : VirtualMachine.list()) { String displayName = vd.displayName(); if (displayName.contains(targetClass)) { VirtualMachine virtualMachine = VirtualMachine.attach(vd); virtualMachine.loadAgent(jarName, filter); virtualMachine.detach(); }
运行方式:先运行agentTarget启动待增强的jvm、再运行agentTest动态增强
@Test public void agentTarget() { HelloService helloService = new HelloService(); while (true) { helloService.sayHi("hi"); try { Thread.sleep(10 * 1000L); } catch (InterruptedException e) { //e.printStackTrace(); @Test public void agentTest() throws Exception { //mvn clean package -Dmaven.test.skip=true , jarName需要修改,改成自己的打包生成路径 String jarName = "/Users/ghost/works/hello/target/hello-1.0-SNAPSHOT-jar-with-dependencies.jar"; String targetClass = "HelloTest"; //需要增强的package正则 String filter = "com.aliyun.demo.+Service"; for (VirtualMachineDescriptor vd : VirtualMachine.list()) { String displayName = vd.displayName(); if (displayName.contains(targetClass)) { VirtualMachine virtualMachine = VirtualMachine.attach(vd); virtualMachine.loadAgent(jarName, filter); virtualMachine.detach(); }agent启动前的console输出
--------------sayHi start--------------- com.aliyun.demo.app.HelloService.sayHi:hi --------------sayHi end------------------ --------------sayHi start--------------- com.aliyun.demo.app.HelloService.sayHi:hi --------------sayHi end------------------agent启动后的console输出
filter:com.aliyun.demo.+Service transform class:com/aliyun/demo/app/HelloService --------------sayHi start--------------- com.aliyun.demo.app.HelloService.sayHi:hi --------------sayHi end------------------ --------------asm 字节码增强信息 start----------- 调用方法:sayHi 方法入参:hi 响应耗时:5 ms --------------asm 字节码增强信息 end-----------注意是HelloService.sayHi,不是HelloServiceQyf.sayHi 简单介绍jvm的部分指令,详情参见https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings spring的代码增强的原理,本质是通过classloader反射+asm字节码增强,去做一个目标类的子类加载到当前jvm 在线字节码工具原理,通过java agent方式动态的修改jvm被增强的class,实现获取入参、异常、返回值、耗时等
【JVM深度解析】字节码指令和存储引擎 字节码指令属于Class文件那个位置?常写的代码后的字节码你知道多少?Integer127的缓存能不能变?...不懂?一文带你深入浅出了解字节码指令和Java存储引擎
两张图让你快速读懂JVM字节码指令 很多人可能会觉得JVM字节码很神秘,我们写的一行行代码放到底层竟然可以用一串16进制的数字保存。再到计算机底层竟然可以用0和1执行如何复杂的代码。JVM的设计确实十分巧妙,但对我们几乎所有开发者来说,这些底层的内容我们已经不需要再去掌握了,因此今天我们不去讲JVM字节码究竟是怎么设计的,我们通过最简单的方法来快速读懂JVM字节码。
JVM 字节码指令解析(下) 概述本文主要是基于 .class 文件,进行分析 .class 文件的内容。 这部分个人觉得主要是属于设计机构拓展的内容,大家可以一起来学习一下 Java 字节码的设计结构以及感受一下设计者的设计。
JVM 字节码指令解析(上) 概述本文主要是基于 .class 文件,进行分析 .class 文件的内容。 这部分个人觉得主要是属于设计机构拓展的内容,大家可以一起来学习一下 Java 字节码的设计结构以及感受一下设计者的设计。
相关文章
- 微服务轮子项目(50) -JVM 分析工具详解
- Java8 JVM内存结构讲解及配置优化
- JVM GC日志(一)
- 看了最新大厂面试,这 6 道 JVM 面试题都被问到了
- 【jvm系列-04】精通运行时数据区共享区域---堆
- 【jvm系列-03】精通运行时数据区私有区域---虚拟机栈、程序计数器、本地方法栈
- JVM 面试题,安排上了!!!
- JVM_06 类加载与字节码技术(类文件结构)
- JVM GC调优一则–增大Eden Space提高性能
- 使用java自带工具监控jvm运行状态
- windows下配置tomcat服务器的jvm内存大小的两种方式
- 面试突击(六)——JVM如何实现JAVA代码一次编写到处运行的?
- 深入JVM锁机制2-Lock
- JVM源码分析之javaagent原理完全解读--转