java7 invokedynamic命令深入研究
在看java虚拟机字节码执行引擎的时候,里面提到了java虚拟机里调用方法的字节码指令有5种:
- invokestatic //调用静态方法
- invokespecial //调用私有方法、实例构造器方法、父类方法
- invokevirtual //调用实例方法
- invokeinterface //调用接口方法,会在运行时再确定一个实现此接口的对象
- invokedynamic //先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
前4种很容易理解,但是第5种笔者本人从这段描述上无法理解这个invokedynamic到底是什么东西,于是决定从实践入手来剖析一下。
invokedynamic本身是字节码命令,我们想直接调用这个命令只能手写java字节码,这个难度太大了。。有没有替换方案呢,答案是有的。
ASM简介
官方的定义:ASM是一个java字节码操作和分析框架。可以用来编辑classes文件和直接动态生成class文件,一切都是直接基于二进制形式的。
我来解释下:我们都知道,一个.java文件编译后会生成.class文件,.java文件中记录代码的形式是java源代码,而.class文件中记录代码的形式是java字节码,这两者本质上是以不同的形式存储相同的内容,两者也可以相关转换(编译和反编译)。而asm本身是一个java库,所以说编写asm代码的时候本质是在写java源代码,但是asm代码的最终目的并不是为了运行,而是为了生成字节码。
举个例子:
我现在有一个Test.java类:
package common; public class Test {
public void say(){
System.out.println("Hi");
}
}
这个类编译后生成Test.class,Test.class文件里存储的实际上就是一个byte数组,但是我们可以用javap -verbose命令翻译为字节码命令查看(忽略常量池等无关信息,只截取say()方法的CODE码):
public void say();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String Hi
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
我标红的部分就是从字节码翻译过来的字节码命令,字节码和字节码命令是一一对应的,如invokevirtual命令在源文件中就是一个字节 0xb6,对应关系可以查表:http://www.cnblogs.com/sheeva/p/6279096.html
看到这里我们会发现,这里的invokevirtual命令就是java调用方法的5种字节码中的第3种,如果我们能够修改这里的invokevirtual改成invokedynamic我们就能搞清楚invokedynamic到底是做什么的了,但是javap命令只能以命令的形式查看字节码却不能修改,这时候就轮到asm登场了。
下载 asm5.2:http://download.forge.ow2.org/asm/asm-5.2-bin.zip
解压后在lib里找到asm-all-5.2.jar放到Test.class的目录下,执行:
java -classpath "./*" org.objectweb.asm.util.ASMifier Test.class,
运行结果如图:
生成了一个java类的源码,里面有一个dump()方法,这个方法返回值是byte[],这个byte[]的内容就是Test.class的字节码,也就是说如果把方法的返回值保存到一个文件,那么这个文件和Test.class文件是完全一样的。
为了验证这个结论,我把这个方法粘贴出来,自己写一个类加载器来加载方法返回的字节码然后调用say()方法:
package invokedynamic; import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; public class Hello implements Opcodes { public static void main(String[] args) throws Exception {
byte[] codes=dump();
Class<?> clazz=new MyClassLoader().defineClass("common.Test", codes);
clazz.getMethod("say", null).invoke(clazz.newInstance(), new Object[]{});
} public static byte[] dump() throws Exception { ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0; cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "common/Test", null, "java/lang/Object", null); {
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hi");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd(); return cw.toByteArray();
} private static class MyClassLoader extends ClassLoader implements Opcodes {
public Class<?> defineClass(String name, byte[] b){
return super.defineClass(name, b, 0, b.length);
} }
}
运行成功:
现在我们确定了,这个dump()方法确实能够生成Test.class的字节码,现在来看一下dump()方法里的内容:
public static byte[] dump() throws Exception { ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0; cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "common/Test", null, "java/lang/Object", null); {
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hi");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd(); return cw.toByteArray();
}
如果熟悉字节码的话,应该已经看出来了,dump()这个方法所在的类实现了Opcodes接口,Opcodes接口里定义了几乎全部的java字节码命令,我们之前说的5个invoke命令也在内:
然后看一下dump()方法里我标红的4句,和之前的javap命令打出来的Test.class字节码命令对照看:
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hi");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
public void say();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String Hi
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
不需要解释了吧。。
用asm调用invokedynamic指令
现在我们来把原来say方法里的通过invokevirtual输出Hi的代码去掉,改成通过invokedynamic输出hello。
在dump()方法所在的类Hello类的包里加一个类Bootstrap:
package invokedynamic; import java.lang.invoke.*; public class Bootstrap { private static void hello() {
System.out.println("Hello!");
} public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.lookup();
Class thisClass = lookup.lookupClass();
MethodHandle mh = lookup.findStatic(thisClass, "hello", MethodType.methodType(void.class));
return new ConstantCallSite(mh.asType(type));
}
}
Hello类把say()方法里原来通过invokevirtual调用System.out.println()方法的那几行去掉,换成动态调用:
{
mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
mv.visitCode(); // mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// mv.visitLdcInsn("Hi");
// mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// mv.visitInsn(RETURN);
// mv.visitMaxs(2, 1); MethodType mt = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class,
MethodType.class);
Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, "invokedynamic/Bootstrap", "bootstrap",
mt.toMethodDescriptorString());
mv.visitInvokeDynamicInsn("dynamicInvoke", "()V", bootstrap);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 1); mv.visitEnd();
}
再次运行,这次输出变了:
结论
现在我们结合我们得到的代码,再重新理解一下invokedynamic的定义:
先在运行时动态解析出调用点限定符所引用的方法, //即通过bootstrap方法动态解析出hello方法
然后再执行该方法, //即执行hello方法
而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 //这里的引导方法,即我们定义的bootstrap方法,这里我们的逻辑是直接分派了hello方法,但是我们也可以写一些逻辑,比如根据调用时候的参数类型来动态决定调用哪个方法
现在我们已经自己实践了invokedynamic命令的使用,但是我相信很多人还是不明白这个命令的意义所在,这要从语言的静态类型和动态类型说起:
静态类型就是每个变量在初始化的时候就要声明唯一的类型并且不能改变。
动态类型就是说变量没有固定类型,变量的类型取决于它里面元素的类型。
java语言是静态类型的。有人可能会提到泛型,java的泛型是擦除式的,也就是说虽然在编写java源码时看起来好像不能确定变量类型,但是在java编译为字节码的过程中,每一个变量都是有确定的类型的。
所以从java语言的角度,之前的4条方法调用指令是完全够用的,但是要知道,jvm不只是跨平台的,还是跨语言的,当有人在jvm上试图开发动态类型语言的时候,问题就来了:
jvm大多数指令都是类型无关的,但是在方法调用的时候,却不是这样,每个方法调用在编译阶段就必须指明方法参数和返回值类型,但是动态类型语言的方法参数,直到运行时刻才能知道类型啊,因此jdk就做了这样一个“补丁”:用invokedynamic调用方法的时候,会转到bootstrap方法,在这个方法里可以动态获取参数类型,然后根据参数类型分派合适的方法作为CallSite(动态调用点),最后真实调用的就是CallSize里的方法。如此便能在jvm上实现动态类型语言的方法调用了。
java7 invokedynamic命令深入研究的更多相关文章
- jvm系列(四):jvm调优-命令大全(jps jstat jmap jhat jstack jinfo)
文章同步发布于github博客地址,阅读效果更佳,欢迎品尝 运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎 ...
- GC参考手册 —— GC 调优(命令篇)
运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎鼎的VisualVM,IBM的Memory Analyzer ...
- [转]jvm调优-命令大全(jps jstat jmap jhat jstack jinfo)
运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎鼎的VisualVM,IBM的Memory Analyzer ...
- Java虚拟机(五):JVM调优命令
运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎鼎的VisualVM,IBM的Memory Analyzer ...
- 加分项——C语言实现Linux的pwd命令
加分项--C语言实现Linux的pwd命令 实现要求 1 学习pwd命令 2 研究pwd实现需要的系统调用(man -k; grep),写出伪代码 3 实现mypwd 4 测试mypwd pwd pw ...
- 第9周 实现PWD命令
第9周 实现PWD命令 码云链接:https://gitee.com/bestiisjava2017/laura5332/blob/master/%E4%BF%A1%E6%81%AF%E5%AE%89 ...
- Linux内建命令和外部命令
Linux命令有内部命令(内建命令)和外部命令之分,内部命令和外部命令功能基本相同,但也有些细微差别. [内部命令 vs. 外部命令] (1)内部命令实际上是shell程序的一部分,其中包含的是一些比 ...
- Linux kdb命令
一.简介 Linux 内核调试器(KDB)允许您调试 Linux 内核.这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构.KDB 的主要优点之一就是它不需要用另一台机器进行调 ...
- jvm系列(四):jvm调优-命令篇
运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎鼎的VisualVM,IBM的Memory Analyzer ...
随机推荐
- strlen sizeof strcat strcpy区别
strlen(p): 能计算出p指向字符串的长度(以当前p的位置开始),不包含终止字符'\0': p可以声明为char* p或者char p[],这两种形式strlen均能正确计算. sizeof ...
- [Unity]C#中 将XML和实体类之间进行相互转换的工具类
using System; using System.Xml; using System.Xml.Serialization; using System.IO; namespace LOTool { ...
- cocopods安装与使用
转自http://www.cnblogs.com/jys509/p/4839803.html Cocoapods安装步骤 1.升级Ruby环境 sudo gem update --system 如果R ...
- iOS获取设备唯一标识的各种方法?IDFA、IDFV、UDID分别是什么含义?
一.UDID (Unique Device Identifier) UDID的全称是Unique Device Identifier,顾名思义,它就是苹果IOS设备的唯一识别码,它由40个字符的字母和 ...
- 【转】程序员必须知道的几个Git代码托管平台
一.VS2013中克隆远程Git仓库和SSH的配置 1.VS2013中克隆远程项目 首先感谢园友的评论和补充,今日又仔细看了一下,VS2013中是可以克隆项目的,只是我一直用的GitHub来克隆的 ...
- java判断是否为汉字
java判断是否为汉字 public static boolean isChinese(String str) { String regEx = "[\u4e00-\u9fa5]&quo ...
- 读书笔记--用Python写网络爬虫02--数据抓取
抓取(scraping)---爬虫从网页中抽取一些数据用以实现某些用途. 三种抽取网页数据的方法:正则表达式.Beautiful Soup和lxml. 2.1 分析网页 通过浏览器自带选项,查看网页源 ...
- ID3算法(Java实现)
数据存储文件:buycomputer.properties #数据个数 datanum=14 #属性及属性值 nodeAndAttribute=年龄:青/中/老,收入:高/中/低,学生:是/否,信誉: ...
- AFNetWorking 之 网络请求的基本知识
NSString *urlStr = @"http://api.openweathermap.org/data/2.5/forecast/daily"; AFHTTPRequest ...
- 浅谈Java工具类CommonUtils的使用
package com.xushouwei.cn; import java.util.HashMap; import java.util.Map; import org.junit.Test; imp ...