有一本用go语言编写的《自己动手写Java虚拟机》的书籍,抛开性能不说,但这本书已经勾勒出虚拟机的模型,对于想真正了解虚拟机执行原理的小伙伴,无疑是雪中送炭,有人会说,我不如直接去研究真正的Java虚拟机,那才叫原汁原味,学习概念模型的虚拟机和真实的虚拟机肯定是有巨大差别的, 但是我想说,如果你真这么牛逼,也不需要看我的博客了,我个人觉得,人的能力精力是有限的,如果花较少的时间和精力,就能对虚拟机有一个大致的了解,等将来有能力,精力,毅力再去研究jdk源码的实现,不失为一种曲线救国的办法,首先,我对《自己动手写Java虚拟机》的作者张秀宏是非常的崇拜的, 将来有机会,还会去拜读其他作品(如《自己手动实现Lua,虚拟机,编译器和标准库》,《 WebAssembly原理与核心技术》 )等, 作者在《自己动手写Java虚拟机》将设计模式用到了极致, 同时代码思路清晰浅显易懂,确实是我辈楷模,鉴于我对作者的崇拜,以及go语言和Java语言特性的区别, 因此,我想用Java语言来实现《自己动手写Java虚拟机》中的代码 , 希望能更加好的推广此书,有人可能会说,go语言不是实现得很好了吗?为什么还要用Java语言来实现一遍呢,不是重复造轮子嘛,多此一举吗?一方面 ,如果Java开发者想读《自己动手写Java虚拟机》这本书,必须去了解Go语言的语法 ,虽然语言特性差不多,但是至少是两门不同的语言,语法是有很大的区别的,下面就来看看两种语言之间的语法区别。
func main() { fmt.Printf("测试一") a := []int{1, 2, 3, 4, 5} b := a[2:] fmt.Println(a) fmt.Println(b) b[1] = 10 fmt.Println(a) fmt.Println(b) fmt.Println("======================================") fmt.Println("测试2") c := make([]int, 5, 5) for i := 0; i < 5; i++ { c[i] = i + 1 } fmt.Println(c) d := c[2:] c = append(c, 6) c = append(c, 7) c = append(c, 8) d[1] = 10 fmt.Println(c) fmt.Println(d) }
首先,我们来理解上面例子的用意 ,申明一个数组a ,赋值{1,2,3,4,5}, 再申明一个切片b,截取数组a[2]之后包括a[2]的值赋值给切片b,这个时候修改b[1]的值为10 ,打印数组a时,发现a[3]的值也变成了10,这个很诡异,在Java中,你拷贝一个数组,修改拷贝的数组,不应该对原来的数组产生影响啊,在go语言中为什么会产生影响呢? 带着疑问,我们继续来看第二个例子。
在第二个例子中,一样,先定义一个数组c ,长度为5,容量为5 ,再给c赋初值{1,2,3,4,5},同样创建一个切片d, 将数组c中c[2]以后(包括c[2])的值,赋给了d,此时再给c增加3个元素{6,7,8}, 之后再修改d[1]的值,也就对应c[3]的值,再打印c,和d ,诡异的事情又发生了c[3]的值不受影响,还是原来的4,发生这样奇怪的现象,原因是什么呢?Java程序员肯定一脸懞,无法理解啊,下面来看看go语言怎样解释这种现象 。
在go语言中,为了节省内存使用,当数组不扩容时,数组指向相同的内存地址,也就是说,数组a,和数组b指向一段相同的内存地址,只是起始引用位置不同,所以修改b[1]的值,当然也修改了a[3]的值。 而在c数组中,申明长度是5,容量是5,当将元素{6,7,8}再加到c数组时,原来的数组容不下新的元素,只能扩容,再申明一个更长的数组,将原来数组的内容拷贝过来,将新的元素添加到新的数组中去。因此d[1]只修改了原来数组的内容,而新数组是重新拷贝的,因此对其中的元素值没有影响,如果Java程序员没有学习过go语言,是不是直接掉到坑里去了。
type User struct { age int } func modify(user User ){ user.age = 30 } func realModify(user * User ){ user.age = 40 } func main() { user:= User{age : 10} fmt.Println("原来的值:" ,user.age) modify(user) fmt.Println("调用modify方法后的值",user.age) realModify(&user); fmt.Println("调用realModify方法后的值",user.age) }
看到上面例子的小伙伴,不妨想想,三次打印的结果是什么 。
我们调用modify方法时,传入的是user对象,在modify中修改了user的age属性值为30,在main方法中打印user.age的值竟然还是10,如果在Java语言中,肯定是30,而之后调用realModify方法,传入的是user对象的地址引用,之后再打印user.age,发现值变成了40,如果学习过c语言的小伙伴,可能理解起来容易一点,但是纯Java开发者可能又懞了,在go语言中,所有的方法都是按值传递,也就是说,在方法中对结构体中的属性进行修改,不会影响到调用者的值,如果想在方法中修改方法参数的值,只能按地址传递,这也是go语言和java语言的一大区别,一不小心就掉坑里了,也就是说,在后面的java版本的自己写java虚拟机的时候,有些地方用了对象的clone方法的原因 。
type UserLog struct { loginIP string loginTime float64 } type User struct { userName string age int UserLog } func main() { user := User{ userName: "zhangsan", age: 20, UserLog: UserLog{ loginIP: "127.0.0.1", loginTime: 132892398, }, } fmt.Println(user.loginIP) }
细心的读者有没有发现,在引用user结构体UserLog对象的loginIP属性时,可以直接通过user.loginIP引用,这也是go语言的语法特性,而在java中一定需要user.getUserLog().getLoginIP()才能获取到对象的属性。 这虽然简单,但是对于复杂的项目来说,还是有点晕的,到底是用了哪个对象的哪个属性。写到最后,可能自己也晕了。 但在《自己动手写Java虚拟机》中有大量的这种语法的使用,所以在读《自己动手写Java虚拟机》示例代码时,需要小心。并不是说这种语法不好,只是我个人现在不太喜欢而已,说不定将来喜欢呢。
还有一些语法特性,比如java中所有的对象都可以用Object来表示 ,而在go中,所有的对象都可以用interface{}来表示 。比如在go语言中没有interface关键字来实现接口,只要你的方法实现了接口中所有的方法,go默认就认为你实现了接口,编译器会自动帮你关联上,同时,在java中有public ,protected ,private等关键字来控制作用域,在go语言中只能通过属性或方法的首字母大小写来定义方法或属性的访问范围等,像这种小小的语法特性的区别还是有很多的,这里就不一一列举了。 总之,如果你是一个java程序员,想来研究《自己动手写Java虚拟机》这本书,是要先学习go语言的基本语法特性才能真正的看懂这本书的源代码,也就才有收获,为了方便大家学习,我自己将自己动手写Java虚拟机中的第11章节中的代码写了一个java版本,希望对大家学习虚拟机有所帮助。话不多说了。作者其实已经写得很好了,建议去看原书,我将我从中学习到的知识记录下来。感兴趣的读者可以继续阅读我的博客 。
先声明,《自己动手写Java虚拟机》中的虚拟机只是一个概念版的虚拟机,所以在性能方面就不做过多的考虑,但能从代码的角度来帮助我们大概的了解虚拟机的运行原理,至少我觉得是一本难得的好书,因此我写的java版本的java虚拟机,也是不考虑性能,只考虑如何简单优雅的实现,可读性强即可 。
第一章的目的其实很简单,就是通过一个类文件,能读取出其中的字节码。
public interface Entry { String PathListSeparator = ":"; // OS-specific path list separator byte[] readClass(String className); }
定义一个Entry接口,这个接口只做一件事情,通过类名,读取类的字节码,那我们的class文件以哪种形式存在呢?以某个目录下直接放了class文件,或者 zip 或jar包中存放的class文件,或从网络中读取class字节流,或从数据库中读取,而我们先只考虑最常见的从某个目录下读取class文件,或者 zip 或jar包中提取class文件。 先来看从目录下直接读取class文件。
public class DirEntry implements Entry { private String absDir; public DirEntry(String absDir) { this.absDir = absDir; } @Override public byte[] readClass(String className) { String classPath = Filepath.join(absDir, StringUtils.getClassName(className)); return ClassReaderUtils.readClass(classPath); } @Override public String toString() { return absDir; } }
从目录中读取class文件的原理很简单,就是找到class文件的绝对路径,调用ClassReaderUtils的readClass工具类方法,得到类的byte[]数组而已。classPath的具体值实际上就是 /Users/username/gitlab/jvm-java/target/test-classes/com/test/ch06/CircleXXX.class的字符串。
下面我们来看对于zip或jar包中的class文件该如何读取。
public class ZipEntry implements Entry { public String absPath; public ZipEntry(String absPath) { this.absPath = absPath; } @Override public byte[] readClass(String className) { try { File file = new File(absPath); JarFile jar = new JarFile(file); Enumeration en = jar.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); String name = je.getName(); if (name.endsWith(StringUtils.getClassName(className))) { InputStream is = jar.getInputStream(je); return ClassReaderUtils.readClass(is); } } } catch (IOException e) { e.printStackTrace(); } return null; } @Override public String toString() { return absPath; } }
从jar包或zip中读取class文件的原理也很简单,遍历jar或zip中的所有class文件,如果文件名和我们传入的文件名相同,则调用ClassReaderUtils的readClass方法读取class文件即可。
读取类的工具类有了,那从哪里读取类文件呢?不知道读者有没有去了解过虚拟机的启动过程,虚拟机启动要好几个类加载器,启动类加载器,扩展类加载器,和用户类加载器,双亲委派模型就是基于这几个加载器之上才能实现的,当然JDK9 的模块加载,这另当别论,我们就来看jdk7之前的类加载过程, 启动类加载器,扩展类加载器,和用户类加载器他们的区别就是加载类的路径不同而已,启动类加载器加载的是/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib下的所有包, 而扩展类加载器加载的就是/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/*下所有包。 而用户类加载器则加载用户指定路径下的包。 因此,如果要去查找一个类的字节码先从启动类加载器的目录下寻找,再到扩展类加载器的目录下寻找,如果还找不到,则到用户指定的目录下寻找,既然思路找到了,那下面来看看代码如何实现。
public class Classpath { private Entry bootClasspath; private Entry extClasspath; private Entry userClasspath; public Classpath(String jreOption, String cpOption) { parseBootAndExtClasspath(jreOption); parseUserClasspath(cpOption); } public void parseBootAndExtClasspath(String jreOption) { String jreDir = getJreDir(jreOption); // jre/lib/* String jreLibPath = Filepath.join(jreDir, "lib", "*"); bootClasspath = FileUtils.newWildcardEntry(jreLibPath); // jre/lib/ext/* String jreExtPath = Filepath.join(jreDir, "lib", "ext", "*"); extClasspath = FileUtils.newWildcardEntry(jreExtPath); } public void parseUserClasspath(String cpOption) { if (cpOption == "") { cpOption = "."; } userClasspath = FileUtils.newEntry(cpOption); } private String getJreDir(String jreOption) { if (jreOption != "" && exists(jreOption)) { return jreOption; } if (exists("./jre")) { return "./jre"; } String javaHome = System.getenv("JAVA_HOME"); if (StringUtils.isNotBlank(javaHome)) { return Filepath.join(javaHome, "jre"); } return null; } public boolean exists(String path) { File file = new File(path); return file.exists(); } }
细心的读者可能一看就明白其中的奥妙,我们已经知道了启动类加载器,扩展类加载器,用户类加载器的加载路径,定义三个Entry,分别是bootClasspath,extClasspath,userClasspath,在Classpath初始化时,加载对应路径下所有的jar包或zip或class文件为Entry,那如果要找一个类,是不是先从bootClasspath下的Entry中寻找,如果找不到再从extClasspath中寻找,一直找到用户指定的目录 。那方法该如何实现呢?请看下面方法。
public byte[] readClass(String className) { byte[] data = bootClasspath.readClass(className); if (data != null) { return data; } data = extClasspath.readClass(className); if (data != null) { return data; } return userClasspath.readClass(className); }
上面的方法正好实现双亲委派模型的思想, 但是Classpath中有一小点需要注意的是,如果用户没有指定JAVA_HOME的路径,则通过System.getenv(“JAVA_HOME”)从环境中获取,细心的读者可能发现了一个问题,我们之前只讲过zip文件的读取,或者绝对路径下的class文件的读取,但是上面启动类加载器读取的是/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/* ,以 * 号结尾的目录,这又是怎样加载的呢?我们先来看newWildcardEntry方法的实现。
public static CompositeEntry newWildcardEntry(String path ){ String baseDir = path.substring(0,path.length()-1) ;// remove * Listentries = new ArrayList<>(); List fileNameList = new ArrayList<>(); File file = new File(baseDir); ergodic(file,fileNameList); for(String fileName :fileNameList){ File f = new File(fileName); if(f.isDirectory() && !fileName.equals(baseDir)){ continue; } if(fileName.endsWith(".jar") || fileName.endsWith(".JAR")){ entries.add(new ZipEntry(fileName)); } } return new CompositeEntry(entries); } public static List ergodic(File file, List resultFileName) { File[] files = file.listFiles(); if (files == null) return resultFileName;// 判断目录下是不是空的 for (File f : files) { if (f.isDirectory()) {// 判断是否文件夹 ergodic(f, resultFileName);// 调用自身,查找子目录 } else resultFileName.add(f.getPath()); } return resultFileName; }
如果没有看代码,可能觉得好像有点难,看了以后,是不是觉得很简单,调用ergodic方法,拿到目录下的所有文件的绝对路径,遍历所有的文件,如果文件以jar 或JAR结尾,则创建一个ZipEntry对象,加入到CompositeEntry的entries集合中。
从上面的分析中,其实找到类的字节码原理就十分简单了,先遍历启动类加载器下的所有jar包,读取jar包中的每一个文件,如果文件名一样,则读取字节码,如果找不到,则到扩展类加载器的目录下寻找,以此类推,直到找到为止。
其中用到了3个Entry ,这三个Entry关系如下。
CompositeEntry中存储着entry的集合,entry可能是CompositeEntry,也可能是ZipEntry或DirEntry,而在找类的过程中,遍历CompositeEntry中所有的entry,调用其readClass寻找,如果找到则返回,真正落到实处获取字节码的是ZipEntry或DirEntry的readClass方法。我相信读到此处,小伙伴已经清楚了第一章的实现,那下面来看一个例子,如何获取String类的字节码。
public class Test1 { public static void main(String[] args) { Cmd cmd = new Cmd(); cmd.setCpOption("/Users/quyixiao/gitlab/jvm-java/target/test-classes/com/test/ch06"); cmd.setXjreOption("/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre"); cmd.setJclass("String"); Classpath cp = new Classpath(cmd.getXjreOption(), cmd.getCpOption()); byte [] b = cp.readClass(cmd.getJclass()); System.out.println(Arrays.toString(b)); } }
上面这个例子的意图就是想打印出String类的字节码,看一下执行结果。
[-54, -2, -70, -66, 0, 0, 0, 52, 0, -102, 10, 0, 30, 0, 89, 9, 0, 32, 0, 90, 10, 0, 91, 0, 92, 7, 0, 46, 10, 0, 15, 0, 93, 10, 0, 12, 0, 94, 10, 0, 95, 0, 96, 10, 0, 95, 0, 97, 10, 0, 12, 0, 98, 10, 0, 12, 0, 99, 10, 0, 20, 0, 100, 7, 0, 101, 10, 0, 12, 0, 102, 10, 0, 103, 0, 92, 7, 0, 104, 10, 0, 15, 0, 105, 10, 0, 12, 0, 106, 7, 0, 107, 10, 0, 18, 0, 108, 7, 0, 109, 5, 0, 0, 0, 0, 127, -1, -1, -1, 7, 0, 110, 10, 0, 23, 0, 111, 7, 0, 112, 10, 0, 18, 0, 113, 10, 0, 18, 0, 114, 10, 0, 18… ]
现在已经有了字节码了,下面我们来分析原书中的第3章,解析class文件。
在解析class文件之前,我们先来看一个例子。
public class ClassFileTest { public static final boolean FLAG = true; public static final byte BYTE = 123; public static void main(String[] args) throws RuntimeException { System.out.println("Hello, World!"); } }
这个例子很简单,就是定义两个静态变量,打印一个Hello, World!, 调用javap -verbose 命令,输出如下。
Classfile /Users/quyixiao/gitlab/jvm-java/target/test-classes/com/test/ch03/ClassFileTest.class Last modified 2021-12-28; size 737 bytes MD5 checksum 1074caff83b73ab9293e3052ce3c1d9a Compiled from "ClassFileTest.java" public class com.test.ch03.ClassFileTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#30 // java/lang/Object."":()V #2 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #33 // Hello, World! #4 = Methodref #34.#35 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #36 // com/test/ch03/ClassFileTest #6 = Class #37 // java/lang/Object #7 = Utf8 FLAG #8 = Utf8 Z #9 = Utf8 ConstantValue #10 = Integer 1 #11 = Utf8 BYTE #12 = Utf8 B #13 = Integer 123 #14 = Utf8 #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 LocalVariableTable #19 = Utf8 this #20 = Utf8 Lcom/test/ch03/ClassFileTest; #21 = Utf8 main #22 = Utf8 ([Ljava/lang/String;)V #23 = Utf8 args #24 = Utf8 [Ljava/lang/String; #25 = Utf8 Exceptions #26 = Class #38 // java/lang/RuntimeException #27 = Utf8 MethodParameters #28 = Utf8 SourceFile #29 = Utf8 ClassFileTest.java #30 = NameAndType #14:#15 // "":()V #31 = Class #39 // java/lang/System #32 = NameAndType #40:#41 // out:Ljava/io/PrintStream; #33 = Utf8 Hello, World! #34 = Class #42 // java/io/PrintStream #35 = NameAndType #43:#44 // println:(Ljava/lang/String;)V #36 = Utf8 com/test/ch03/ClassFileTest #37 = Utf8 java/lang/Object #38 = Utf8 java/lang/RuntimeException #39 = Utf8 java/lang/System #40 = Utf8 out #41 = Utf8 Ljava/io/PrintStream; #42 = Utf8 java/io/PrintStream #43 = Utf8 println #44 = Utf8 (Ljava/lang/String;)V { public static final boolean FLAG; descriptor: Z flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 1 public static final byte BYTE; descriptor: B flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 123 public com.test.ch03.ClassFileTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/test/ch03/ClassFileTest; public static void main(java.lang.String[]) throws java.lang.RuntimeException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello, World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 16: 0 line 17: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; Exceptions: throws java.lang.RuntimeException MethodParameters: Name Flags args } SourceFile: "ClassFileTest.java"
在上一章节中,我们知道.class文件以字节码的形式存在,那字节码和javap -verbose转化出来的文件的转化关系是什么呢?而解析class文件就是将byte[]的字节码转化为上述文件的一个过程。
首先,我们来简略的看看class文件结构。
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
我相信看过java字节码的小伙伴对上面的结构肯定了如指掌,magic u4 表示魔数占4个字节,紧接着次版本号,主版本号,再接着常量池的个数点两个字节,人这里我们能知道,一个类常量池最多只能有(2 ^ 8 * 2^8 )-1 = 65535个,紧接着从字节码中读取每一个常量,读完常量之后,就是类的访问标识符access_flags占两个字节,这个访问标识怎样来理解呢?我们还是看之前的ClassFileTest类,这个类的访问标识flags是【flags: ACC_PUBLIC, ACC_SUPER】
从上图中,我们得到ClassFileTest的access_flags为0 33 ,转化为十进制即33 ,而33由0x0001 & 0x0020 = 0x0021而来,因此能推算出类的访问标识即 ACC_PUBLIC, ACC_SUPER。紧接着来看this_class占两个字节,难道是当前类名由两个字节组成吗?当然不是,u2表示指向常量池中的索引为u2,为什么this_class占两个字节,不是占4个字节,也不是占一个字节,相信聪明的你一下子就明白了,因为常量池个数就是占两个字节,如果设计为4个字节,浪费空间,设计成1个字节,当类索引超过一个字节时,则无法表示了。 字节码后面的内容,我就不一一分析了,之前我也写了一篇博客 Java字节码文件结构剖析(一)感兴趣的小伙伴可以去研究一下,怎样手动一个一个字节的去分析类中的字节码,对于你理解以代码的方式解析字节码有极大帮助。字节码后面的内容,我就不一一的分析了,我们来以代码的方式实现解析class文件, 在真正的解析字节码之前,我们先做一个铺垫,在python,go语言中,比较常用的一种语法就是元组,但是java中是没有元组的,下面来看go语言中元组的使用。
import "fmt" func test() (int ,string ){ return 1 ,"aa" } func main() { a ,b := test() fmt.Println(a,b) }
调用test()方法返回2个元素,如果在java中肯定不能这样写的,但想要元组这种结构,我也只能用泛型来写一个假元组了,下面来看看java中元组的实现。
public class TestTuple { public static Tuple2test(){ return new Tuple2(1,"a"); } public static void main(String[] args) { Tuple2 data = test(); System.out.println(data.getFirst() + " ," +data.getSecond()); } }
虽然没有那样简捷,但也只能揍合着用吧。
go语言中有uint8,uint16,uint32,uint64 4种基本数据类型,在java中没有,那只能模拟了,因此创建4个类,分别是
public class Uint8 { private int value; public Uint8(int value) { this.value = value; } public int Value() { return value; } } public class Uint16 { private int value; public Uint16(int value) { this.value = value; } public int Value() { return value; } } public class Uint32 { private int value; public Uint32(int value) { this.value = value; } public int Value() { return value; } } public class Uint64 { private Long value; public Uint64(Long value) { this.value = value; } public Long Value() { return value; } }
uint8,uint16,uint32可以用int来表示,但是uint64 占8个字节,在java中只能用long类型来存储数据了,准备工作已经做完了,下面我们来看看代码是如何解析class文件的。
创建ClassReader工具类
public class ClassReader { byte[] data; public ClassReader(byte[] data) { this.data = data; } // u1 public Uint8 readUint8() { int value = readByte8(0); copyArray(1); return new Uint8(value); } ... public byte[] readBytes(int n) { byte[] data1 = new byte[n]; byte[] data2 = new byte[this.data.length - n]; System.arraycopy(this.data, 0, data1, 0, n); System.arraycopy(this.data, n, data2, 0, data.length - n); this.data = data2; return data1; } public void copyArray(int readerByte) { byte[] newData = new byte[data.length - readerByte]; System.arraycopy(this.data, readerByte, newData, 0, data.length - readerByte); this.data = newData; } private int readByte8(final int index) { return this.data[index] & 0xFF; } private int readByte16(final int index) { return ((this.data[index] & 0xFF) << 8) | (this.data[index + 1] & 0xFF); } private int readByte32(final int index) { return ((this.data[index] & 0xFF) << 24) | ((this.data[index + 1] & 0xFF) << 16) | ((this.data[index + 2] & 0xFF) << 8) | (this.data[index + 3] & 0xFF); } private long readByte64(final int index) { long l1 = readByte32(index); long l0 = readByte32(index + 4) & 0xFFFFFFFFL; return (l1 << 32) | l0; } }
首先,我们来看看读取1个字节的方法readUint8,readByte8方法,主要是向data中读取第0个字节,读取完之后,移除掉第0个字节,在go语言中,可以直接通过data=data[1:]即可得到新的数组,而java中,只能通过数组copy来移除掉第0个字节,这样保证this.data中的数据都是没有被读取过的,每次只需要从第0个字节开始读取,可读性更强,但是性能肯定不好,在Spring 中用ASM读取字节码的时候,在类中维护着一个索引,每次从索引位置开始读取,当然性能更高,但是实现起来更加复杂,在本章中,侧重于读者的理解,因此忽略了性能方面考虑。
我们定义了一个类,类结构如下,定义一个read方法,read方法就是解析class文件结构的实现。
public class ClassFile { public Uint32 magic; public Uint16 minorVersion; public Uint16 majorVersion; public ConstantPool constantPool; public Uint16 accessFlags; public Uint16 thisClass; public Uint16 superClass; public Uint16[] interfaces; public MemberInfo[] fields; public MemberInfo[] methods; public AttributeInfo[] attributes; public void read(ClassReader reader) { this.readAndCheckMagic(reader); this.readAndCheckVersion(reader); this.constantPool = ConstantInfoUtils.readConstantPool(reader); this.accessFlags = reader.readUint16(); this.thisClass = reader.readUint16(); this.superClass = reader.readUint16(); this.interfaces = reader.readUint16s(); this.fields = AttributeInfoUtils.readMembers(reader, this.constantPool); this.methods = AttributeInfoUtils.readMembers(reader, this.constantPool); this.attributes = AttributeInfoUtils.readAttributes(reader, this.constantPool); } }
在read方法中,我们首先来看第一个方法readAndCheckMagic() 读取并较验魔数。
public void readAndCheckMagic(ClassReader reader) { Uint32 magic = reader.readUint32(); if (magic.Value() != 0xCAFEBABE) { ExceptionUtils.throwException("魔数不正确 "); } }
上面的代码非常简单,就是从字节码中读取4个字节,看字节内容是不是0xCAFEBABE,如果不是则抛出异常,所有class文件都是以0xCAFEBABE开头,约定俗成的东西,这里不做过多解释。
接下来,我们继续来看readAndCheckVersion()读取并较验版本号。
public void readAndCheckVersion(ClassReader reader) { this.minorVersion = reader.readUint16(); this.majorVersion = reader.readUint16(); switch (this.majorVersion.Value()) { case 45: return; case 46: case 47: case 48: case 49: case 50: case 51: case 52: if (this.minorVersion.Value() == 0) { return; } } ExceptionUtils.throwException("java.lang.UnsupportedClassVersionError!"); }
分别从字节码中读取两个字节,但是需要注意的是,次版本号在前,主版本号在后。
接下来就是常量池的读取,常量池的读取是通过readConstantPool()方法来实现,接下来,我们来看ConstantInfoUtils类的具体实现。
public class ConstantInfoUtils { public static final int CONSTANT_Class = 7; public static final int CONSTANT_Fieldref = 9; public static final int CONSTANT_Methodref = 10; public static final int CONSTANT_InterfaceMethodref = 11; public static final int CONSTANT_String = 8; public static final int CONSTANT_Integer = 3; public static final int CONSTANT_Float = 4; public static final int CONSTANT_Long = 5; public static final int CONSTANT_Double = 6; public static final int CONSTANT_NameAndType = 12; public static final int CONSTANT_Utf8 = 1; public static final int CONSTANT_MethodHandle = 15; public static final int CONSTANT_MethodType = 16; public static final int CONSTANT_InvokeDynamic = 18; public static ConstantPool readConstantPool(ClassReader reader) { Uint16 cpCount = reader.readUint16(); ConstantInfo constantInfos[] = new ConstantInfo[cpCount.Value()]; ConstantPool cp = new ConstantPool(); cp.setConstantInfos(constantInfos); // The constant_pool table is indexed from 1 to constant_pool_count - 1. for (int i = 1; i < cpCount.Value(); i++) { cp.constantInfos[i] = ConstantInfoUtils.readConstantInfo(reader, cp); // http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.5 // All 8-byte constants take up two entries in the constant_pool table of the class file. // If a CONSTANT_Long_info or CONSTANT_Double_info structure is the item in the constant_pool // table at index n, then the next usable item in the pool is located at index n+2. // The constant_pool index n+1 must be valid but is considered unusable. if (cp.constantInfos[i] instanceof ConstantLongInfo || cp.constantInfos[i] instanceof ConstantDoubleInfo) { i++; } } return cp; } }
我们定义了CONSTANT_Class, CONSTANT_Fieldref, CONSTANT_Methodref等,为什么要定义那么多的常量呢?请看下图。
字节码中第一个8位表示当前是class还是fieldref 还是methodref 等。 但每个常量占用的字节可能不同,因此作者写了如下代码,先读取tag,判断当前是哪种类型的常量,再调用每种类型的readInfo方法 。不过上述还需要注意的一点是,对于 double,和long类型,在常量池中占2个槽。因此在读取到ConstantLongInfo和ConstantDoubleInfo时,i 要++
public static ConstantInfo readConstantInfo(ClassReader reader, ConstantPool cp) { Uint8 tag = reader.readUint8(); ConstantInfo c = newConstantInfo(tag, cp); c.readInfo(reader); return c; } public static ConstantInfo newConstantInfo(Uint8 tag, ConstantPool cp) { switch (tag.Value()) { case CONSTANT_Integer: return new ConstantIntegerInfo(); case CONSTANT_Float: return new ConstantFloatInfo(); case CONSTANT_Long: return new ConstantLongInfo(); case CONSTANT_Double: return new ConstantDoubleInfo(); case CONSTANT_Utf8: return new ConstantUtf8Info(); case CONSTANT_String: return new ConstantStringInfo(cp); case CONSTANT_Class: return new ConstantClassInfo(cp); case CONSTANT_Fieldref: return new ConstantFieldrefInfo(new ConstantMemberrefInfo(cp)); case CONSTANT_Methodref: return new ConstantMethodrefInfo(new ConstantMemberrefInfo(cp)); case CONSTANT_InterfaceMethodref: return new ConstantInterfaceMethodrefInfo(new ConstantMemberrefInfo(cp)); case CONSTANT_NameAndType: return new ConstantNameAndTypeInfo(); case CONSTANT_MethodType: return new ConstantMethodTypeInfo(); case CONSTANT_MethodHandle: return new ConstantMethodHandleInfo(); case CONSTANT_InvokeDynamic: return new ConstantInvokeDynamicInfo(); default: ExceptionUtils.throwException("java.lang.ClassFormatError: constant pool tag!"); } return null; }
因此,定义一个ConstantInfo接口,接口中定义两个方法readInfo和Value方法。readInfo主要从字节码中读取常量值,Value方法返回对应的常量值。
/* cp_info { u1 tag; u1 info[]; } */ public interface ConstantInfo{ void readInfo(ClassReader reader); T Value(); }
下面来看一下他们之间的关系 。太多了,只截取了一部分。感兴趣小伙伴,可以下载代码自行查看。
由于篇幅有限,我们不能对每一个常量解析的代码进行说明,这里我们就挑选CONSTANT_Utf8来作为例子解析。
@Slf4j public class ConstantUtf8Info implements ConstantInfo { public String str; @Override public void readInfo(ClassReader reader) { Uint16 length = reader.readUint16(); byte bytes[] = reader.readBytes(length.Value()); try { this.str = new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error("异常", e); } } @Override public Object Value() { return null; } public String Str() { return this.str; } }
首先,我们知道常量utf8的结构如下,第一个字节标识当前常量是utf-8,前面已经读取过 , 两个字节表示当前utf-8占几个字节,读取length长度的byte就是utf-8的实际内容。因此上面的代码,第一步是读取2个字节,再读取length长度的byte转化为String对象,从这个例子中,我们至少可以知道,在java中,字符串长度不能超过65535的长度,因为只占两个字节,16位。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
读取完常量池之后,紧接着是对accessFlags,thisClass,superClass,interfaces的读取,这几个属性读取没有什么特别的地方,分别从字节码中读取两个字节,接着是对属性的读取,接着看代码 。
public class AttributeInfoUtils { // read field or method table public static MemberInfo[] readMembers(ClassReader reader, ConstantPool cp) { Uint16 memberCount = reader.readUint16(); MemberInfo members[] = new MemberInfo[memberCount.Value()]; for (int i = 0; i < members.length; i++) { members[i] = readMember(reader, cp); } return members; } public static MemberInfo readMember(ClassReader reader, ConstantPool cp) { return new MemberInfo( cp, reader.readUint16(), reader.readUint16(), reader.readUint16(), readAttributes(reader, cp) ); } public static AttributeInfo[] readAttributes(ClassReader reader, ConstantPool cp) { Uint16 attributesCount = reader.readUint16(); AttributeInfo[] attributes = new AttributeInfo[attributesCount.Value()]; for (int i = 0; i < attributes.length; i++) { attributes[i] = readAttribute(reader, cp); } return attributes; } public static AttributeInfo readAttribute(ClassReader reader, ConstantPool cp) { Uint16 attrNameIndex = reader.readUint16(); String attrName = cp.getUtf8(attrNameIndex); Uint32 attrLen = reader.readUint32(); AttributeInfo attrInfo = newAttributeInfo(attrName, attrLen, cp); attrInfo.readInfo(reader); return attrInfo; } public static AttributeInfo newAttributeInfo(String attrName, Uint32 attrLen, ConstantPool cp) { switch (attrName) { case "BootstrapMethods": return new BootstrapMethodsAttribute(); case "Code": return new CodeAttribute(cp); case "ConstantValue": return new ConstantValueAttribute(); case "Deprecated": return new DeprecatedAttribute(); case "EnclosingMethod": return new EnclosingMethodAttribute( cp); case "Exceptions": return new ExceptionsAttribute(); case "InnerClasses": return new InnerClassesAttribute(); case "LineNumberTable": return new LineNumberTableAttribute(); case "LocalVariableTable": return new LocalVariableTableAttribute(); case "LocalVariableTypeTable": return new LocalVariableTypeTableAttribute(); case "SourceFile": return new SourceFileAttribute(cp); case "Synthetic": return new SyntheticAttribute(); default: return new UnparsedAttribute(attrName, attrLen, null); } } }
看了上述方法之后,我相信有些读者可能有点懞,为什么呢,为什么fields和methods都是通过readMembers()方法来赋值呢?下面我们来看看field和method的结构。
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
细心的读者肯定会发现他们的结构基本上一样,从Java语言的角度上来讲,也是一样的啊,无论是属性还是方法都有访问标识符,名称和描述符。其余的就是属性信息了。 因此其都可以通过readMembers()方法来解析,而真正的解析属性信息时,和常量池的解析设计模式一样,我们还是挑其中一个来分析一下吧。就挑Code结构来分析。
public class CodeAttribute implements ConstantInfo, AttributeInfo { private ConstantPool cp; private Uint16 maxStack; private Uint16 maxLocals; private byte[] code; private ExceptionTableEntry exceptionTable[]; private AttributeInfo[] attributes; public CodeAttribute() { } public CodeAttribute(ConstantPool cp) { this.cp = cp; } @Override public void readInfo(ClassReader reader) { this.maxStack = reader.readUint16(); this.maxLocals = reader.readUint16(); Uint32 codeLength = reader.readUint32(); this.code = reader.readBytes(codeLength.Value()); this.exceptionTable = readExceptionTable(reader); this.attributes = AttributeInfoUtils.readAttributes(reader, cp); } @Override public Object Value() { return null; } public ExceptionTableEntry[] readExceptionTable(ClassReader reader) { Uint16 exceptionTableLength = reader.readUint16(); ExceptionTableEntry exceptionTable[] = new ExceptionTableEntry[exceptionTableLength.Value()]; for (int i = 0; i < exceptionTable.length; i++) { exceptionTable[i] = new ExceptionTableEntry( reader.readUint16(), reader.readUint16(), reader.readUint16(), reader.readUint16()); } return exceptionTable; } public int MaxStack() { return this.maxStack.Value(); } public int MaxLocals() { return this.maxLocals.Value(); } public byte[] Code() { return this.code; } public ExceptionTableEntry[] ExceptionTable() { return this.exceptionTable; } public LineNumberTableAttribute LineNumberTableAttribute() { for (AttributeInfo attrInfo : this.attributes) { if (attrInfo instanceof LineNumberTableAttribute) { return (LineNumberTableAttribute) attrInfo; } } return null; } }
上面代码看得似懂非懂吧,还是要将code的字节码结构摆出来,大家才会更加清楚 。
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
当看到方法的属性结构时,大家可能感觉到困惑,Code_attribute中明显第一位和第二位分别是attribute_name_index和attribute_length,但在readInfo中并没有提及啊,细心的读者肯定会发现,原来是在readAttribute()方法中已经读取了,先读取两位标识当前是BootstrapMethods还是Code,再读取4个字节的长度,因为方法和field的属性名不止有代码中写的这几种,还有其他的,因为我们没有用到,因此只能从字节码中读取attrLen个byte并抛弃掉,从而保证后面的读取正确。当我们看到Code_attribute的结构,再来看CodeAttribute的readInfo方法,相信很简单了。这里就不再一一说明。下图即方法表结构。
其他的属性表结构的解析,大同小异,这里就不再一一列举了,感兴趣的小伙伴可以自己去下载源码研究。
我相信看到这里的小伙伴,肯定跃跃欲试了,代码设计是不错,但是实不实用,就要看测试结果了,那我们来看一个例子。还是对刚刚的ClassFileTest类进行解析。
public class Test3 { public static void main(String[] args) { Cmd cmd = new Cmd(); cmd.setCpOption("/Users/quyixiao/gitlab/jvm-java/target/test-classes/com/test/ch03"); cmd.setXjreOption("/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre"); cmd.setJclass("ClassFileTest"); Classpath cp = new Classpath(cmd.getXjreOption(), cmd.getCpOption()); byte [] b = cp.readClass(cmd.getJclass()); System.out.println(Arrays.toString(b)); ClassFile cf = ClassFile.Parse(b); } }
第四章中主要讲Thread , Stack , Frame , LocalVars , OperandStack Slot 他们之间的对应关系。 代码目录结构如下。
他们实际关系是什么呢?我画一个图如下 。
Slot只是存储数据一个最小单元,下面来看一下Slot对象的结构。
public class Slot implements Cloneable{ public int num; public JObject ref; }
Slot中有两个属性,一个是存储数值类型,一个是对象类型。如果当前操作数是数值类型,则取num的值,如果当前操作数是对象,则取ref的值 。
每一个线程中都有一个栈,每一个栈中可能有多个帧,帧与帧之间以链表的形式链接,如下图所示
每一个Frame我们可以看成是一个方法,每一个帧中都有操作数栈和本地变量表。说这么多,可能读者还是有点晕,不如来看一个例子。如下:
public class Test04 { public static void main(String[] args) { int a = 1; int b = 2; int area = a + b ; } }
iconst_1,istore_1,这些指令目前还没有介绍,先提前试用一下,后面再来分析。
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
下面就对上面每一条指令进行分析。
1: istore_1
int val = frame.OperandStack().PopInt(); // 此时val = 1
frame.LocalVars().SetInt(1, val);
2: iconst_2
frame.OperandStack().PushInt(2);
3: istore_2
int val = frame.OperandStack().PopInt();
frame.LocalVars().SetInt(2, val);
4: iload_1
int val = frame.LocalVars().GetInt(index);
frame.OperandStack().PushInt(val);
5: iload_2
int val = frame.LocalVars().GetInt(index); //
frame.OperandStack().PushInt(val);
6: iadd
OperandStack stack = frame.OperandStack();
Integer v2 = stack.PopInt();
Integer v1 = stack.PopInt();
Integer result = v1 + v2; // 1 + 2 = 3
stack.PushInt(result);
7: istore_3
int val = frame.OperandStack().PopInt();
frame.LocalVars().SetInt(3, val);
每一个线程都存在一个栈,栈中存储着多个帧,当执行新的方法时,创建一个新的帧入栈,在编译期间就计算好了帧中局部变量表数量和操作数栈的个数。实际方法的执行,就是按Code中的指令一条条执行即可 。看到这里,我相信大家对他们之间的关系和执行过程有一定了解,这里就不贴代码了,感兴趣的小伙们,可以自己去下载代码研究具体的实现。
Java虚拟机就是一台机器,而字节码(bytecode)就是运行在这台虚拟机上的机器码。 字节码中存放编码后的Java虚拟机指令,每条指令都以一个单字节的操作码(opcode )开头,这就是字节码名称由来,由于只使用一个字节表示操作码,因此,Java虚拟机最多只支持256(2^8)条指令,为了方便记忆,Java虚拟机规范给了每个操作码都指定了一个助词符(Mnemonic),比如操作码是0x00这条指令。他就什么都不做。
Java虚拟机使用了变长指令,操作码后面可以跟零字节或多个字节的操作数(operand),如果把指令想象成函数的话,操作数就是它的参数,为了编码后的字节码更加紧凑,很多的操作码本身就隐含了操作数,比如常数0推入操作数栈的指令就是iconst_0
在第4章中就已经讨论过,操作数栈和局部变量表只存放数据的值,并不记录数据的类型,结果就是指令必须知道自己在操作什么类型的数据,这一点也直接反映在操作码的助词符上,例如 ,iadd指令就是对int值进行加法操作,dstore指令把操作数栈顶的double值弹出,存储到局部变量表中,areturn从方法中返回引用值,如果某个指令可以操作不同的类型的变量,则助词符的第一个字母表示变量类型,助词符的首字母和变量类型之间的对应关系如表 5-1 所示 。
Java虚拟机规范把已经定义的205条指令按用途分成了11类,分别是:常量(constants)指令,加载(loads)指令,存储(stores)指令,操作数栈(stack)指令,数学(math)指令,转换(conversions)指令,比较(comparisons)指令,控制(control)指令,引用(references)指令,扩展(extended)指令和保留(reserved)指令。
保留指令一共有3条,其中一条是留给调试器的,用于实现断点,操作码是202(0xCA),助词符是breakpoint,另外两条留给Java虚拟机实现内部使用,操作码分别是254(0xFE)和266(0xFF),助词符是impdep1和impdep2,这3条指令是不会出现在class文件中。
在代码中创建一个instructions的包,在包中实现每一条指令。之前我们说java中有11类指令,现在就挑选每一类中的每一条指令来进行说明吧。不过在分析之前,我们还是来看需要做哪些准备。
我们知道每一条指令分为两部分,第一部分是从字节码中读取操作数,第二部分就是执行,因此,我们需要创建一个接口,接口中两个方法,第一个方法从字节码中提取操作数,第二个参数就是执行具体的指令逻辑。
public interface Instruction { void FetchOperands(BytecodeReader reader); //FetchOperands()方法从字节码中提取操作数 void Execute(Frame frame); //Execute()方法 执行指令逻辑 }
所有的指令直接或间接的实现了Instruction接口。
我们需要考虑一个问题,比如ICONST_0,ICONST_1,ICONST_2,ICONST_3这种指令,是不需要从字节码中提取操作数的,因此,创建一个抽象类,实现FetchOperands()的空方法。
//NoOperandsInstruction表示没有操作数的指令,所以没有定义 public abstract class NoOperandsInstruction extends AbstractInstruction implements Instruction { @Override public void FetchOperands(BytecodeReader reader) { // empty } }
对于和本地变量相关指令,一般要从本地变量表中存储或加载变量,因此需要从字节码中读取一个字节的长度,因此我们需要写一个Index8Instruction的抽象类,来从字节码中读取8位的长度。
public abstract class Index8Instruction extends AbstractInstruction implements Instruction { public int Index; @Override public void FetchOperands(BytecodeReader reader) { this.Index = reader.ReadUint8().Value(); } }
对于那些操作数是常量池中的变量的指令,因为常量池的长度占16位,两个字节,因此,我们还需要写一个读取16位byte的抽象类。
//有一些指令需要访问运行时常量池,常量池索引由两字节操 作数给出。把这类指令抽象成Index16Instruction结构体, //用Index字 段表示常量池索引。FetchOperands()方法从字节码中读取一个 uint16整数,转成uint后赋给Index字段 public abstract class Index16Instruction extends AbstractInstruction implements Instruction { public int Index; @Override public void FetchOperands(BytecodeReader reader) { this.Index = reader.ReadUint16().Value(); } }
最典型的是不是NEW指令,new ${XXX} ,XXX来自于常量池,因上需要读取2个字节。 像ReadUint8()方法和ReadUint16()这些方法的实现细节这里就不再赘述,和之前的字节码读取的方式一样。
下面我们将为每一种类型的指令挑选出一个代表来进行分析 。
public class ICONST_0 extends NoOperandsInstruction { @Override public void Execute(Frame frame) { frame.OperandStack().PushInt(0); } }
ICONST_0指令的意义很简单,就是将一个int 类型的 0 推送到操作数栈顶,当然还有ICONST_1,ICONST_2,ICONST_3,ICONST_4,ICONST_5,虚拟机为什么会设计这6条指令呢,因为0,1,2,3,4,5这些常量用得比较多,如果指令本身就携带了操作数,一方面节省class字节码占用空间,另一方面提升性能,因此对于ICONST_0这种指令是自己携带操作数的,第一位是I表示int类型。
// Load reference from local variable //aload系列指令 操作引用类型变量 public class ALOAD extends Index8Instruction { @Override public void Execute(Frame frame) { _aload(frame, this.Index); } } public void _aload(Frame frame, int index) { JObject ref = frame.LocalVars().GetRef(index); frame.OperandStack().PushRef(ref); }
ALOAD表示从局部变量表中加载一个引用类型的变量推入操作数栈顶,为什么ALOAD会继承Index8Instruction呢?聪明的读者肯定想到,因为本地变量表的长度就只能是2^8次方个,因此this.Index的值肯定是0-255之内的数,看到代码,对ALOAD一系列指令的意义应该一目也然了。
public class FSTORE extends Index8Instruction { @Override public void Execute(Frame frame) { _fstore(frame, this.Index); } } public void _fstore(Frame frame, int index) { float val = frame.OperandStack().PopFloat(); frame.LocalVars().SetFloat(index, val); }
FSTORE的意义是不是一目了然,从操作数栈顶中弹出一个符点数保存到本地变量表索引为index的变量槽中。
// Duplicate the top operand stack value public class DUP extends NoOperandsInstruction { /* bottom -> top [...][c][b][a] \_ | V [...][c][b][a][a] */ @Override public void Execute(Frame frame) { OperandStack stack = frame.OperandStack(); Slot slot = stack.PopSlot(); Slot slot1 = slot.clone(); stack.PushSlot(slot); stack.PushSlot(slot1); } }
DUP指令,相当于在复制操作数栈顶元素再推入到操作数栈中,一点需要注意的是,java中是按引用传递的,因此slot需要调用clone方法,这一点和go语言有点小区别。
// Add int @Slf4j public class IADD extends NoOperandsInstruction { @Override public void Execute(Frame frame) { OperandStack stack = frame.OperandStack(); Integer v2 = stack.PopInt(); Integer v1 = stack.PopInt(); Integer result = v1 + v2; stack.PushInt(result); } }
最具代表性的就是IADD指令了,a + b , 从操作数栈顶中弹出a ,b ,两者相加 再推入到操作数栈顶。
// Convert double to float public class D2F extends NoOperandsInstruction { @Override public void Execute(Frame frame) { OperandStack stack = frame.OperandStack(); Double d = stack.PopDouble(); Float f = d.floatValue(); stack.PushFloat(f); } }
随便看一个转换指令 D2F ,将double类型转化为float类型, 从操作数栈顶中弹出一个double类型的变量,转化为float类型,再入操作数栈顶。
public class IF_ICMPLE extends BranchInstruction { @Override public void Execute(Frame frame) { Tuple2data = _icmpPop(frame); if (data.getFirst() <= data.getSecond()) { Base.Branch(frame, this.Offset); } } } public Tuple2 _icmpPop(Frame frame) { OperandStack stack = frame.OperandStack(); Integer val2 = stack.PopInt(); Integer val1 = stack.PopInt(); return new Tuple2<>(val1,val2); }
指令IF_ICMPLE理解可能有点难度,this.Offset是什么呢?其实这个是pc的偏移量。可能这样说,大家还是有点晕,我们先来看一个例子吧。
public class GaussTest { public static void main(String[] args) { int i= 0 ; if(i > 5 ){ i ++; }else { i --; } } } Code: stack=2, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: iconst_5 4: if_icmple 13 7: iinc 1, 1 10: goto 16 13: iinc 1, -1 16: return
java代码的意思很简单了,定义一个变量i,如果i > 5 ,则i ++ ,否则 i – , Code的实现方式是
0: iconst_0:将0推入操作数栈顶
1: istore_1:将0从操作数栈中弹出并存储到本地变量表的第1个位置
2: iload_1 :加载本地变量表中第1个位置的变量推入到操作数栈顶
3: iconst_5:将int类型为5的常量推入到操作数栈顶。
4: if_icmple 13:从操作数栈中弹出 5 和1 ,如果1 > 5 则跳转到pc = 13 的位置,也就是i – ,那么this.Offset的值就是9 ,因为4 + 9 = 13。
7: iinc 1, 1 : i++
10: goto 16 : 跳转到return 指令
13: iinc 1, -1 :i –
相信此时,你对比较指令有了一定的了解了。
控制指令,我们还是经典的switch指令
// Access jump table by key match and jump public class LOOKUP_SWITCH implements Instruction { public Integer defaultOffset; public Integer npairs; public int[] matchOffsets; @Override public void FetchOperands(BytecodeReader reader) { reader.SkipPadding(); this.defaultOffset = reader.ReadInt32(); this.npairs = reader.ReadInt32(); this.matchOffsets = reader.ReadInt32s(this.npairs * 2);//matchOffsets有点像Map,它的key是case值,value是跳转偏移 量。 } //Execute()方法先从操作数栈中弹出一个int变量,然后用它查找 matchOffsets,看是否能找到匹配的key。如果能,则按照value给出的 偏移量跳转,否则按照defaultOffset跳转 @Override public void Execute(Frame frame) { Integer key = frame.OperandStack().PopInt(); int a = this.defaultOffset; int b = frame.thread.pc; for (int i = 0; i < this.npairs * 2; i += 2) { if (this.matchOffsets[i] == key.intValue()) { int offset = this.matchOffsets[i + 1]; Base.Branch(frame, offset); return; } } Base.Branch(frame, this.defaultOffset); } } public static void Branch(Frame frame, int offset) { int pc = frame.Thread().PC(); int nextPC = pc + offset; frame.SetNextPC(nextPC); }
还是先来看一个例子吧。
public class GaussTest { public static void main(String[] args) { int i = 2; switch (i) { case 1: i++; break; case 2: i--; break; default: i = i + 2; break; } } } main方法对应的code Code: stack=2, locals=2, args_size=1 0: iconst_2 1: istore_1 2: iload_1 3: lookupswitch { // 2 1: 28 2: 34 default: 40 } 28: iinc 1, 1 31: goto 44 34: iinc 1, -1 37: goto 44 40: iload_1 41: iconst_2 42: iadd 43: istore_1 44: return
idea中看一下效果
从上图中,我们看到defaultOffset=37,npairs=2 , matchOffsets={1,25,2,31} ,而key = 2 ,frame.thread.pc = 3 ,如果都不匹配,则跳转到37 + 3 = 40 ,也就是Code中的
40: iload_1这一行,如果key 和1匹配,则跳转到3 +25 = 28 ,则会执行
28: iinc 1, 1
31: goto 44,
如果key=2 ,则跳转到 31 + 3 = 34 ,则执行
34: iinc 1, -1
37: goto 44 指令,通过这个例子,我相信大家对控制指令有一定了解了,接下来,来看引用指令
public class NEW extends Index16Instruction { @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); ClassRef classRef = (ClassRef) cp.GetConstant(this.Index); JClass jClass = classRef.symRef.ResolvedClass(); if (!jClass.InitStarted() ){ frame.RevertNextPC(); //另外,如果解析后的类还没有初始化,则需要先初始化类。 在第7章实现方法调用之后会详细讨论类的初始化,这里暂时先忽 略。 ClassInitLogic.InitClass(frame.Thread(), jClass); return; } //因为接口和抽象类都不能实例化,所以如果解析后的类是接 口或抽象类,按照Java虚拟机规范规定,需要抛出InstantiationError 异常。 if (jClass.IsInterface() || jClass.IsAbstract()) { ExceptionUtils.throwException("java.lang.InstantiationError"); } JObject ref = jClass.NewObject(); frame.OperandStack().PushRef(ref); } }
虽然很多的知识点还没有讲,但是大致的可以看出NEW指令的执行流程,从常量池中获取类引用,再创建对象,推入到操作数栈顶,因为方法,类这些都在后面的篇幅中才做分析说明了,但读者也能大概的明白这里面的意思了。
// Branch if reference is null public class IFNULL extends BranchInstruction { //根据引用是否是null进行跳转,ifnull和ifnonnull指令把栈顶的 引用弹出。 @Override public void Execute(Frame frame) { Object ref = frame.OperandStack().PopRef(); if (ref == null) { Base.Branch(frame, this.Offset); } } }
IFNULL指令和比较指令类似,如果为null,则根据偏移量跳转。
每一条指令的具体实现,可以去下载我的代码查看,也可以去看作者的《自己动手写Java虚拟机》这本书,在学习完本章之后,我们来看一个例子。
public class GaussForEache { public static void main(String[] args) { int sum = 0 ; for(int i = 0 ;i < 1 ;i ++){ sum += i ; } } }
相信初学者都知道上面的代码执行结果,我们来看一下执行的指令集是怎样子的
Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: iconst_1 6: if_icmpge 19 9: iload_1 10: iload_2 11: iadd 12: istore_1 13: iinc 2, 1 16: goto 4 19: return
不知道此时此刻,能否说明上面每一条指令的执行过程了。通过stack 和locals 知道当前操作数栈有2个,本地变量表有3个,下面就一条条的来解释执行上面的每条指令 。
看了上面的分析,下面再来看代码的具体执行过程,相信小伙伴们就不觉得那么难了。
com/test/ch05/GaussForEache.main() #0 ICONST_0 {} com/test/ch05/GaussForEache.main() #1 ISTORE_1 {} com/test/ch05/GaussForEache.main() #2 ICONST_0 {} com/test/ch05/GaussForEache.main() #3 ISTORE_2 {} com/test/ch05/GaussForEache.main() #4 ILOAD_2 {} com/test/ch05/GaussForEache.main() #5 ICONST_1 {} com/test/ch05/GaussForEache.main() #6 IF_ICMPGE {Offset=13} com/test/ch05/GaussForEache.main() #9 ILOAD_1 {} com/test/ch05/GaussForEache.main() #10 ILOAD_2 {} com/test/ch05/GaussForEache.main() #11 IADD {} com/test/ch05/GaussForEache.main() #12 ISTORE_1 {} com/test/ch05/GaussForEache.main() #13 IINC{Index=2, Const=1} com/test/ch05/GaussForEache.main() #16 GOTO {Offset=-12} com/test/ch05/GaussForEache.main() #4 ILOAD_2 {} com/test/ch05/GaussForEache.main() #5 ICONST_1 {} com/test/ch05/GaussForEache.main() #6 IF_ICMPGE {Offset=13} com/test/ch05/GaussForEache.main() #19 RETURN {}
是不是明白了,为什么 #16之后会走#4 呢?因为调用了GOTO语句,16+(-12)= 4 ,因此继续重复上面的指令,ILOAD_2 表示加载i的值,因为经过了一轮循环,此时i= 1 , ICONST_1 将1推入操作数栈顶,发现 1 > 1 = false,因此直接跳转到 6+13(this.Offset) = 19 ,也就直接执行return指令了。
了解了指令集,我们再来看一个更加复杂一点的例子
public class TestFinally { public static int test(){ int i = 1; try{ i = 2 ; int a = 0 ; int b = 1; int c = b / a ; return i ; }catch (Exception e ){ i = 3 ; return i ; }finally { i = 4 ; } } public static void main(String[] args) { int i = test(); System.out.println(i); } }
假如代码没有抛出异常,test()方法的返回值是多少,如果抛出异常,返回值又是多少。感兴趣的小伙伴可以自己去测试一下,看结果是不是预期的,如果不是预期的,为什么?
第二章实现了类路径,可以找到class文件,并把数据加载到内存中,第三章实现了class文件的解析,可以把class数据解析成一个ClassFile结构体,本章将进一步处理ClassFile结构体,把它加以转换,放进方法区以供后续使用,本章还会初步讨论和对象的设计,实现一个简单的类加载器。并且实现类和对象相关的部分指令 。
方法区,它是运行时数据区的一块逻辑区域,由多个线程共享,方法区主要存放从class文件获取的类信息,类变量也存放在方法区中,当Java虚拟机第一次使用某个类时,它会搜索类路径,找到相应的class文件,然后读取并解析class文件,把相关的信息放进方法区,至于方法区到底位于何处,是固定大小还是动态调整,是否参与垃圾回收,以及如何在方法区内存放类数据等,Java虚拟机规范并没有明确规定。
public class JClass { public Uint16 accessFlags; // public String name;// string // thisClassName public String superClassName; //父类名称 public String[] interfaceNames; //接口名称数组 public JConstantPool constantPool;//类常量池 public JField[] fields; //所有的属性 public JMethod[] methods; // 方法 public JClassLoader loader; //存放类加载器 指针 public JClass superClass; // 字段存放类的超类指针 public JClass interfaces[]; //存放接口指针 public int instanceSlotCount; //实例变量占据的空间大小 public int staticSlotCount; // 存放类变量空间大小 public Slots staticVars; //字段存放静态变 量 public boolean initStarted; //为了判断类是否已经初始化,需要给Class结构体添加一个字段 public JObject jObject; //通过jClass字段,每个Class结构体实例都与一个类对象关联。 public String sourceFile ; }
第六章主要是将ClassFile结构里的数据转化为类结构,类结构代码 如上所示,每一个字段的含义注释中已经写了,这里就不再赘述,但是这些字段是怎样初始化的呢?
public JClass(ClassFile cf) { this.accessFlags = cf.AccessFlags(); this.name = cf.ClassName(); this.superClassName = cf.SuperClassName(); //从常量池中获取即可 this.interfaceNames = cf.InterfaceNames(); // 从常量池中获取即可 this.constantPool = new JConstantPool(this, cf.constantPool); this.fields = newFields(this, cf.fields); this.methods = newMethods(this, cf.methods); this.sourceFile = getSourceFile(cf) ; //从class文件中读取源文件名 }
accessFlags,name,superClassName,interfaceNames只是直接调用ClassFile的get方法获取即可,接下来,我们来看类常量池的初始化操作。
public class JConstantPool { public JClass jClass; public Object consts[]; public JConstantPool(JClass jClass, ConstantPool cp) { int cpCount = cp.constantInfos.length; this.consts = new Object[cpCount]; this.jClass = jClass; ConstantInfo[] cfCp = cp.constantInfos; for (int i = 1; i < cpCount; i++) { ConstantInfo cpInfo = cfCp[i]; if (cpInfo instanceof ConstantIntegerInfo) { ConstantIntegerInfo intInfo = (ConstantIntegerInfo) cpInfo; consts[i] = intInfo.Value(); } else if (cpInfo instanceof ConstantFloatInfo) { ConstantFloatInfo intInfo = (ConstantFloatInfo) cpInfo; consts[i] = intInfo.Value(); } else if (cpInfo instanceof ConstantLongInfo) { //如果是long或double型常量,也是直接提取常量值放进consts 中。但是要注意,这两种类型的常量在常量池中都是占据两个位 置,所以索引要特殊处理 ConstantLongInfo longInfo = (ConstantLongInfo) cpInfo; consts[i] = longInfo.Value(); i++; } else if (cpInfo instanceof ConstantDoubleInfo) { ConstantDoubleInfo doubleInfo = (ConstantDoubleInfo) cpInfo; consts[i] = doubleInfo.Value(); i++; } else if (cpInfo instanceof ConstantStringInfo) {//如果是字符串常量,直接取出Go语言字符串,放进consts中, ConstantStringInfo stringInfo = (ConstantStringInfo) cpInfo; consts[i] = stringInfo.String(); } else if (cpInfo instanceof ConstantClassInfo) {//还剩下4种类型的常量需要处理,分别是类、字段、方法和接口 方法的符号引用 ConstantClassInfo classInfo = (ConstantClassInfo) cpInfo; consts[i] = new ClassRef(this, classInfo); } else if (cpInfo instanceof ConstantFieldrefInfo) { ConstantFieldrefInfo fieldrefInfo = (ConstantFieldrefInfo) cpInfo; consts[i] = new FieldRef(this, fieldrefInfo); } else if (cpInfo instanceof ConstantMethodrefInfo) { ConstantMethodrefInfo methodrefInfo = (ConstantMethodrefInfo) cpInfo; consts[i] = new MethodRef(this, methodrefInfo); } else if (cpInfo instanceof ConstantInterfaceMethodrefInfo) { ConstantInterfaceMethodrefInfo methodrefInfo = (ConstantInterfaceMethodrefInfo) cpInfo; consts[i] = new InterfaceMethodRef(this, methodrefInfo); } else { } } } public Object GetConstant(int index) { return this.consts[index]; } }
我们已经在newConstantInfo()方法中获取了常量值并赋值给了ClassFile.ConstantPool的constantInfos属性值,因此在这里转变为JClass.JConstantPool的consts属性值,需要注意的是对于Long和Double在常量池中占两个字节,因此在上面的方法中,当遇到ConstantLongInfo和ConstantDoubleInfo时,i 也相应的多加一个1, 对于ConstantStringInfo类型的常量,直接从常量池中获取对应索引的UTF8字符串即可,对于 ClassRef,FieldRef,MethodRef,InterfaceMethodRef对象的结构及构建,我们后面的篇幅再来分析。
在这里提一下,为了避免和Java类名冲突,在这里很多的名称都加上了J ,如JClass对应的是Java中的Class,JObject对应的是Java中的Object,等等,读者在阅读时留意一下即可 。
接下来,我们来看JField的初始化 。
//修改newFields()方法,从字段属性表中读取constValueIndex, 代码改动如下: public JField[] newFields(JClass jClass, MemberInfo[] cfFields) { JField fields[] = new JField[cfFields.length]; for (int i = 0; i < fields.length; i++) { MemberInfo cfField = cfFields[i]; fields[i] = new JField(); fields[i].classMember.jClass = jClass; fields[i].classMember.copyMemberInfo(cfField); fields[i].copyAttributes(cfField); } return fields; } public class JField { public ClassMember classMember; public int constValueIndex; public int slotId; public JField() { if (this.classMember == null){ this.classMember = new ClassMember(); } } public void copyAttributes(MemberInfo cfField) { ConstantValueAttribute valAttr = cfField.ConstantValueAttribute(); if (valAttr != null) { this.constValueIndex = valAttr.ConstantValueIndex().Value(); } } } public class ClassMember { public Uint16 accessFlags; public String name; public String descriptor; public JClass jClass; //class字段存放 Class结构体指针,这样可以通过字段或方法访问到它所属的类。 public String signature ; public byte [] annotationData ;// RuntimeVisibleAnnotations_attribute //copyMemberInfo()方法从class文件中复制数据 public void copyMemberInfo(MemberInfo memberInfo) { this.accessFlags = memberInfo.AccessFlags(); this.name = memberInfo.Name(); this.descriptor = memberInfo.Descriptor(); } public ClassMember(Uint16 accessFlags, String name, JClass jClass) { this.accessFlags = accessFlags; this.name = name; this.jClass = jClass; } }
上面的一些复制和赋值操作也不难理解,这里就不过多说明,但是有没有发现,在JFeild中有一个ClassMember属性,而ClassMember中又有访问标识,名称,描述符,方法或属性所在类,signature,以及注解数据,ClassMember的设计目的是为什么呢?之前我们是不是提到过,在类中Field和Method的结构非常相似,都有上面提到的这些特性,因此,作者就设计了一个ClassMember来共用,你会发现在JMethod也同样有classMember属性,既然分析到这里了,我相信大家对下图也已经理解了。
虽然方法和属性的结构相似,但是还是有众多不同的,来看下面为JClass中的methods赋值操作。
//code字段存放方法字节码。 newMethods()函数根据class文件中的方法信息创建Method表 public JMethod[] newMethods(JClass jClass, MemberInfo[] cfMethods) { JMethod methods[] = new JMethod[cfMethods.length]; for (int i = 0; i < cfMethods.length; i++) { MemberInfo cfMethod = cfMethods[i]; methods[i] = new JMethod(); methods[i].classMember.jClass = jClass; methods[i].classMember.copyMemberInfo(cfMethod); methods[i].copyAttributes(cfMethod);//maxStack、maxLocals和字节码在class文件中 是以属性的形式存储在method_info结构中的。 MethodDescriptorParser parser = new MethodDescriptorParser(); MethodDescriptor md = parser.parseMethodDescriptor(methods[i].classMember.descriptor); methods[i].parsedDescriptor = md ; methods[i].calcArgSlotCount(); //先计算argSlotCount字段,如果是本地 方法,则注入字节码和其他信息。 if (methods[i].IsNative() ){ methods[i].injectCodeAttribute(md.returnType); } } return methods; } public class JMethod { public ClassMember classMember; public int maxStack; // maxStack和maxLocals字段分别存放操作数栈和局部变量表大 小,这两个值是由Java编译器计算好的。 public int maxLocals; public byte[] code; public int argSlotCount; public ExceptionTable exceptionTable; public LineNumberTableAttribute lineNumberTable; public ExceptionsAttribute exceptions; public byte[] parameterAnnotationData; // RuntimeVisibleParameterAnnotations_attribute public byte[] annotationDefaultData;// AnnotationDefault_attribute public MethodDescriptor parsedDescriptor; public JMethod() { if (this.classMember == null) { this.classMember = new ClassMember(); } } }
在理解copyAttributes()方法之前,我们先来看一个简单的例子。
public static void test(){ try { int a = 1; } catch (Exception e) { e.printStackTrace(); } finally { } }
上述方法的字节码结构如下:
public static void test(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=0 0: iconst_1 1: istore_0 2: goto 16 5: astore_0 6: aload_0 7: invokevirtual #3 // Method java/lang/Exception.printStackTrace:()V 10: goto 16 13: astore_1 14: aload_1 15: athrow 16: return Exception table: from to target type 0 2 5 Class java/lang/Exception 0 2 13 any 5 10 13 any LineNumberTable: line 9: 0 line 14: 2 line 10: 5 line 11: 6 line 14: 10 line 12: 13 line 14: 14 line 15: 16 LocalVariableTable: Start Length Slot Name Signature 6 4 0 e Ljava/lang/Exception; StackMapTable: number_of_entries = 3 frame_type = 69 /* same_locals_1_stack_item */ stack = [ class java/lang/Exception ] frame_type = 71 /* same_locals_1_stack_item */ stack = [ class java/lang/Throwable ] frame_type = 2 /* same */
再来看copyAttributes()方法,是不是一目了解,无非将CodeAttribute中的maxStack,maxLocals,code,lineNumberTable,exceptionTable等赋值给JMethod的属性。
public void copyAttributes(MemberInfo cfMethod) { CodeAttribute codeAttr = cfMethod.CodeAttribute(); if (codeAttr != null) { this.maxStack = codeAttr.MaxStack(); this.maxLocals = codeAttr.MaxLocals(); this.code = codeAttr.Code(); this.lineNumberTable = codeAttr.LineNumberTableAttribute(); this.exceptionTable = new ExceptionTable(codeAttr.ExceptionTable(), this.classMember.Class().constantPool); } this.exceptions = cfMethod.ExceptionsAttribute(); this.classMember.annotationData = cfMethod.RuntimeVisibleAnnotationsAttributeData(); this.parameterAnnotationData = cfMethod.RuntimeVisibleParameterAnnotationsAttributeData(); this.annotationDefaultData = cfMethod.AnnotationDefaultAttributeData(); }
不过,上面我们需要注意RuntimeVisibleAnnotationsAttributeData()此类方法,我们在newAttributeInfo()方法中并没有对RuntimeVisibleAnnotations的属性名进行处理,都归类于UnparsedAttribute属性,因此只需要遍历ClassFile的attributes属性,如果属性名和RuntimeVisibleAnnotations相等,则获取其byte数据即可 。
public byte[] RuntimeVisibleAnnotationsAttributeData() { return this.getUnparsedAttributeData("RuntimeVisibleAnnotations"); } public byte[] getUnparsedAttributeData(String name) { for (AttributeInfo attrInfo : this.attributes) { if (attrInfo instanceof UnparsedAttribute) { UnparsedAttribute unparsedAttr = (UnparsedAttribute) attrInfo; if (unparsedAttr.equals(name)) { return unparsedAttr.info; } } } return null; }
而对于行号表,我们在newAttributeInfo()方法中已经处理了LineNumberTable的属性名,因此,直接遍历attributes,并且类型是LineNumberTableAttribute的返回即可。
public LineNumberTableAttribute LineNumberTableAttribute() { for (AttributeInfo attrInfo : this.attributes) { if (attrInfo instanceof LineNumberTableAttribute) { return (LineNumberTableAttribute) attrInfo; } } return null; }
接下来,我们来看方法参数变量槽的个数计算,首先我们来看this.classMember.descriptor的值是什么sukbb,例如 (Ljava/lang/ClassLoader;)V,表示方法的请求参数是java/lang/ClassLoader;对象,返回值为Void类型。因此,我们只需要知道方法的参数类型的个数就能计算出参数变量槽的个数,但需要注意,对于double类型和long类型的参数argSlotCount需要多加1。
public void calcArgSlotCount() { MethodDescriptorParser parser = new MethodDescriptorParser(); MethodDescriptor parsedDescriptor = parser.parseMethodDescriptor(this.classMember.descriptor); for (String paramType : parsedDescriptor.parameterTypes) { this.argSlotCount++; if (paramType == "J" || paramType == "D") { this.argSlotCount++; } } if (!this.classMember.IsStatic()) { this.argSlotCount++; // `this` reference } }
对于native方法,在Java底层是通过JNI来实现,在后面的篇幅中我们再来介绍作者是怎样模拟native的实现,但这里先为Code赋值,方便全面native方法的实现。
public void injectCodeAttribute(String returnType) { this.maxStack = 4; // 因为本地方法帧的 局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals 字段刚好。 this.maxLocals = this.argSlotCount; //至于code字段,也就是本地方法的字节码,第一条指令 都是0xFE,第二条指令则根据函数的返回值选择相应的返回指令。 switch (returnType.toCharArray()[0]) { case 'V': this.code = new byte[]{(byte) 0xfe, (byte) 0xb1}; // return 0xfe = 15 * 16 + 14 break; case 'L': case '[': this.code = new byte[]{(byte) 0xfe, (byte) 0xb0}; // areturn break; case 'D': this.code = new byte[]{(byte) 0xfe, (byte) 0xaf}; // dreturn break; case 'F': this.code = new byte[]{(byte) 0xfe, (byte) 0xae};// freturn break; case 'J': this.code = new byte[]{(byte) 0xfe, (byte) 0xad}; // lreturn break; default: this.code = new byte[]{(byte) 0xfe, (byte) 0xac};// ireturn break; } }
还剩4种类型常量需要处理,分别是类,字段,方法和接口方法的符号引用。
首先来看类符号引用,因为4种类型的符号引用有一些共性,所以仍然使用继承来减少重复的代码。创建类SymRef。
public class SymRef { public JConstantPool cp; //cp字段存放符号引用所在的运行时常量池指针,这样就可以通 过符号引用访问到运行时常量池,进一步又可以访问到类数据。 public String className; //className字段存放类的完全限定名。 public JClass jClass; //class字段缓存解析后的类结构体指针,这样类符号引用只需要解析一次就可以了,后续可以直接使用缓存值。 }
cp字段存放符号引用所在的运行时常量池指针,这样就可以通过符号引用访问到运行时常量池,进一步又可以访问到类数据,className字段存放的完全限定名,class字段缓存解析后的类结构体指针,这样类符号引用只需要解析一次就可以了,后续可以直接使用缓存值,对于类符号引用,只要有类名,就可以解析符号引用,对于字段首先要解析类符号引用得到类数据,然后用字段名和描述符查找字段数据,方法符号引用的解析过程和字段符号的引用类似。
接下来,我们来看类符号引用 。
public class ClassRef { public SymRef symRef; //ClassRef继承了SymRef,但是并没有添加任何字段。 //newClassRef()函数根据class文件中存储的类常量创建ClassRef实 例 public ClassRef(JConstantPool cp, ConstantClassInfo classInfo) { this.symRef = new SymRef(); this.symRef.cp = cp; this.symRef.className = classInfo.Name(); } }
类符号引用,没有定义多余的属性,直接定义一个symRef属性即可,在初始化时为symRef赋值即可,可能聪明的读者发现其实ClassRef是可以直接继承SymRef,而没有必要像go语言一样,在ClassRef添加一个属性SymRef,来达到继承的效果,我觉得还是尽量保持go语言的风格吧。就不再做这些改动。
public class FieldRef { public MemberRef memberRef; public JField field; //Field结构体比较简单,目前所有信息都是从ClassMember中继 承过来的。newFields()函数根据class文件的字段信息创建字段表 //field字段缓存解析后的字段指针,newFieldRef()方法创建 FieldRef实例 public FieldRef(JConstantPool cp, ConstantFieldrefInfo refInfo) { this.memberRef = new MemberRef(); this.memberRef.symRef.cp = cp; this.memberRef.copyMemberRefInfo(refInfo.constantMemberrefInfo); } } public class MemberRef { public SymRef symRef; public String name; //在Java中,我们并不能在同一个类中定义 名字相同,但类型不同的两个字段,那么字段符号引用为什么还要 存放字段描述符呢?答案是, // 这只是Java语言的限制,而不是Java 虚拟机规范的限制。也就是说,站在Java虚拟机的角度,一个类是 完全可以有多个同名字段的,只要它们的类型互不相同就可以。 public String descriptor; //copyMemberRefInfo()方法从class文件内存储的字段或方法常量中 提取数据 public void copyMemberRefInfo(ConstantMemberrefInfo refInfo) { this.symRef.className = refInfo.ClassName(); Tuple2data = refInfo.NameAndDescriptor(); this.name = data.getFirst(); this.descriptor = data.getSecond(); } }
本来应该是FieldRef继承MemberRef,MemberRef再继承SymRef,但将MemberRef作为FieldRef的属性存在,感兴趣的小伙伴可以自行去改。但MemberRef中也中规中矩的出现了field应该有的属性field的name和field的descriptor。copyMemberRefInfo方法只是为name和descriptor赋值而已。
public class MethodRef { public MemberRef memberRef; public JMethod method; public MethodRef(JConstantPool cp, ConstantMethodrefInfo refInfo) { this.memberRef = new MemberRef(); this.memberRef.symRef.cp = cp; this.memberRef.copyMemberRefInfo(refInfo.constantMemberrefInfo); } } public class InterfaceMethodRef { public MemberRef memberRef; public JMethod method; public InterfaceMethodRef(JConstantPool cp, ConstantInterfaceMethodrefInfo refInfo) { this.memberRef = new MemberRef(); this.memberRef.symRef.cp = cp; this.memberRef.copyMemberRefInfo(refInfo.constantMemberrefInfo); } }
MethodRef和InterfaceMethodRef的结构和FieldRef结构类似,这里就不再多说 。那他们之间的关系如下。
6.3 类加载器
Java虚拟机的类加载器十分复杂,在这里将实现一个简化版的类加载器。下面来看代码的具体实现。
/* class names: - primitive types: boolean, byte, int ... - primitive arrays: [Z, [B, [I ... - non-array classes: java/lang/Object ... - array classes: [Ljava/lang/Object; ... */ @Slf4j public class JClassLoader { public Classpath cp; // ClassLoader依赖Classpath来搜索和读取class文件,cp字段保存 Classpath指针 public MapclassMap; // loaded classes //classMap字段记录已经加载的类数据,key是类的完 全限定名。 public boolean verboseFlag ; //是否打印类加载日志用的。 public JClassLoader(Classpath cp, boolean verboseFlag) { this.cp = cp; this.classMap = new HashMap<>(); this.verboseFlag = verboseFlag; this.loadBasicClasses(); this.loadPrimitiveClasses(); } }
JClassLoader依赖Classpath来搜索和读取class文件,cp字段保存了Classpath指针,classMap字段记录已经加载的类的数据,key是类的完全限定名。在前面的讨论中,方法区一直是一个抽象的概念,现在可以把classMap字段作为方法区的一个具体实现。
public void loadBasicClasses() { JClass jlClassClass = this.LoadClass("java/lang/Class"); for (Map.Entrymap : this.classMap.entrySet()) { JClass clazz = map.getValue(); if (clazz.jObject == null) { clazz.jObject = jlClassClass.NewObject(); clazz.jObject.extra = clazz; } } }
loadBasicClasses()函数先加载java.lang.Class类,这又会触发java.lang.Object等类和接口的加载。然后遍历classMap,给已经加载 的每一个类关联类对象。
再来看基本数据类型的加载。
public static MapprimitiveTypes = new HashMap(); static { primitiveTypes.put("void", "V"); primitiveTypes.put("boolean", "Z"); primitiveTypes.put("byte", "B"); primitiveTypes.put("short", "S"); primitiveTypes.put("int", "I"); primitiveTypes.put("long", "J"); primitiveTypes.put("char", "C"); primitiveTypes.put("float", "F"); primitiveTypes.put("double", "D"); } public void loadPrimitiveClasses() { for ( Map.Entry primitiveType : ClassNameHelper. primitiveTypes .entrySet()){ //loadPrimitiveClasses()方法加载void和基本类型的类 this.loadPrimitiveClass(primitiveType.getKey()); } } public void loadPrimitiveClass(String className) { JClass clazz = new JClass( new Uint16(Constants.ACC_PUBLIC), className, this, true); clazz.jObject = this.classMap.get("java/lang/Class").NewObject(); clazz.jObject.extra = clazz; this.classMap.put(className, clazz); }
第一,void和基本类型的类名就是void、 int、float等。第二,基本类型的类没有超类,也没有实现任何接口。第三,基本类型的类对象是通过ldc指令加载到操作数栈中,而基本类型的类对象,虽然在Java代码中看起来是通过字面量获取的,但是编译之后的指令并不是ldc,而是getstatic。每个基本类型都有一个包装类,包装类中 有一个静态常量,叫作TYPE,其中存放的就是基本类型的类,也就是说,基本类型的类是通过getstatic指令访问相应包装类的TYPE字段加载到操作数栈中的。
基本数据类型加载完了,我们来看普通类型的数据加载。
public JClass LoadClass(String name) { JClass jClass = this.classMap.get(name); if (jClass != null) { return jClass; } JClass clazz = null; if (name.toCharArray()[0] == '[') { // array class ,如果是数组类 clazz = this.loadArrayClass(name); } else { clazz = this.loadNonArrayClass(name); } JClass jlClassClass = this.classMap.get("java/lang/Class"); if (jlClassClass != null) { clazz.jObject = jlClassClass.NewObject(); clazz.jObject.extra = clazz; } return clazz; }
这个方法的实现非常简单,如果是数组类型,则调用loadArrayClass()方法,如果非数组类型,则调用loadNonArrayClass方法来加载,接下来,我们来看loadNonArrayClass方法的实现。
public JClass loadNonArrayClass(String name) { byte[] data = this.cp.readClass(name); JClass jClass = this.defineClass(data); link(jClass); //log.info("[Loaded " + name + " from ]"); return jClass; }
readClass()方法是不是非常相似,不是我们在第二章分析过的,先从启动类加载器的路径下查找类,再从扩展类加载器的路径中查找,再到用户指定的路径下查找,最终找到类的字节码信息。而parseClass()方法就是将类字节码转化为ClassFile,拿到了ClassFile,此时我们是不是可以去初始化JClass了。最后将生成的jClass保存到classMap中,下次使用时就不需要再重新加载了。
public JClass defineClass(byte[] data) { JClass jClass = parseClass(data); hackClass(jClass); jClass.loader = this; resolveSuperClass(jClass); resolveInterfaces(jClass); this.classMap.put(jClass.name, jClass); return jClass; } public JClass parseClass(byte[] data) { ClassFile cf = ClassFile.Parse(data); return new JClass(cf); }
虽然只有了了的几千代码,但我觉得作者还是非常牛逼的,用了浅显易懂的代码,实现了复杂的业务逻辑。
接下来,我们来看一下类在加载过程中一个非常重要的过程,链接。
//类的链接分为验证和准备两个必要阶段,link()方法的代码 public void link(JClass jClass) { //为了确保安全性,Java虚拟机规范要求在执行类的任何代码之前,对类进行严格的验证。 verify(jClass); prepare(jClass); } //作者省略了verify的过程,确实太复杂了,有兴趣的小伙伴可以对着Java虚拟机规范自行去实现。 public void verify(JClass jClass) { } // jvms 5.4.2 public void prepare(JClass jClass) { //calcInstanceFieldSlotIds()函数计算实例字段的个数,同时给它们编号 calcInstanceFieldSlotIds(jClass); calcStaticFieldSlotIds(jClass); allocAndInitStaticVars(jClass); }
先来看实例变量的计算方式 。只要数一下类的字段即可。假设某个类有m个静态字段和n个实例字段,那么静态变量和实例变量所需的空间大小就分别是m’和n’。这里要注意两点。首先,类是可以继承 的。也就是说,在数实例变量时,要递归地数超类的实例变量;其次,long和double字段都占据两个位置,所以m’>=m,n’>=n。
public void calcInstanceFieldSlotIds(JClass jClass) { int slotId = 0; if (jClass.superClass != null) { slotId = jClass.superClass.instanceSlotCount; } for (JField field : jClass.fields) { if (!field.classMember.IsStatic()) { field.slotId = slotId; slotId++; if (field.isLongOrDouble()) { slotId++; } } } jClass.instanceSlotCount = slotId; }
静态变量就好说了,遍历所有的属性,如果是静态变量,则变量槽的数量+1,如果是double或long类型,则变量槽再+1 。
public void calcStaticFieldSlotIds(JClass jClass) { int slotId = 0; for (JField field : jClass.fields) { if (field.classMember.IsStatic()) { field.slotId = slotId; slotId++; //Field结构体的isLongOrDouble()方法返回字段是否是long或 double类型 if (field.isLongOrDouble()) { slotId++; } } } jClass.staticSlotCount = slotId; }
顾名思义,allocAndInitStaticVars()方法主要对public static final x 的变量初始化操作。值从哪里来,无非从常量池中来。所以,读者可以看到很多的cp.GetConstant(cpIndex)操作,就是从常量池中获取值,初始化类的静态final属性。
//allocAndInitStaticVars()函数给类变量分配空间,然后给它们赋予初始值 public void allocAndInitStaticVars(JClass jClass) { jClass.staticVars = new Slots(jClass.staticSlotCount); for (JField field : jClass.fields) { if (field.classMember.IsStatic() && field.classMember.IsFinal()) { initStaticFinalVar(jClass, field); } } } //因为Go语言会保证新创建的Slot结构体有默认值(num字段是 0,ref字段是nil),而浮点数0编码之后和整数0相同,所以不用做任 何操作就可以保证静 //态变量有默认初始值(数字类型是0,引用类型 是null)。如果静态变量属于基本类型或String类型,有final修饰符, 且它的值在编译期已知, //则该值存储在class文件常量池中。 initStaticFinalVar()函数从常量池中加载常量值,然后给静态变量 赋值 public void initStaticFinalVar(JClass jClass, JField field) { Slots vars = jClass.staticVars; JConstantPool cp = jClass.constantPool; int cpIndex = field.ConstValueIndex(); int slotId = field.SlotId(); if (cpIndex > 0) { switch (field.classMember.Descriptor()) { case "Z": case "B": case "C": case "S": case "I": int val = (int) cp.GetConstant(cpIndex); vars.SetInt(slotId, val); break; case "J": long vall = (long) cp.GetConstant(cpIndex); vars.SetLong(slotId, vall); break; case "F": float valf = (float) cp.GetConstant(cpIndex); vars.SetFloat(slotId, valf); break; case "D": double vald = (double) cp.GetConstant(cpIndex); vars.SetDouble(slotId, vald); break; case "Ljava/lang/String;": String goStr = (String) cp.GetConstant(cpIndex); JObject jStr = StringPool.JString(jClass.Loader(), goStr); vars.SetRef(slotId, jStr); } } }
在代码中有很多的IsPublic(),IsPrivate() ,IsStatic(),IsFinal()的方法,我们来挑一个看看。
public boolean IsPublic() { return 0 != (this.accessFlags.Value() & Constants.ACC_PUBLIC); }
不知道大家明白上面这个代码的用意了没有,如public访问标识用二进制表示 , ACC_PUBLIC=0b 0000 0000 0001 ,static用二进制表示为 ACC_STATIC= 0b 0000 0000 1000 , final用二进制表示ACC_FINAL= 0b 0000 0000 0001 0000 ,那么public static final 变量的访问标识为 ACC_PUBLIC & ACC_STATIC & ACC_FINAL = 0b 0000 0000 0001 1001 ,如果访问标识再和 (public的二进制) 0b 0000 0000 0001 相与的话,肯定是不等于0的。其他的访问标识都以此类推。
接下来,我们来看异常表的实现。在看异常表实现的代码之前,我们依然看一个例子。如下
public class ExceptionTest { public static void main(String[] args) { try { int a = 0; if (a == 1) { throw new ExceptionA(); } a = 3; if (a == 2) { throw new ExceptionB(); } a = 4; } catch (ExceptionA e) { e.printStackTrace(); } catch (ExceptionB b) { b.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }
上面代码的实现逻辑非常简单,意义就不多说了。下面来看字节码是怎样子的。
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: iconst_1 4: if_icmpne 15 7: new #2 // class com/test/ch06/ExceptionA 10: dup 11: invokespecial #3 // Method com/test/ch06/ExceptionA."":()V 14: athrow 15: iconst_3 16: istore_1 17: iload_1 18: iconst_2 19: if_icmpne 30 22: new #4 // class com/test/ch06/ExceptionB 25: dup 26: invokespecial #5 // Method com/test/ch06/ExceptionB."":()V 29: athrow 30: iconst_4 31: istore_1 32: goto 56 35: astore_1 36: aload_1 37: invokevirtual #6 // Method com/test/ch06/ExceptionA.printStackTrace:()V 40: goto 56 43: astore_1 44: aload_1 45: invokevirtual #7 // Method com/test/ch06/ExceptionB.printStackTrace:()V 48: goto 56 51: astore_1 52: aload_1 53: invokevirtual #9 // Method java/lang/Exception.printStackTrace:()V 56: return Exception table: from to target type 0 32 35 Class com/test/ch06/ExceptionA 0 32 43 Class com/test/ch06/ExceptionB 0 32 51 Class java/lang/Exception
通过上面的代码得知,指令0-32抛出ExceptionA,则直接跳转到pc=35的指令执行,如果0-32指令抛出了ExceptionB,则直接跳转到pc=43的指令执行,如果以上异常都不满足,则跳转到pc=51行的指令执行。
public class ExceptionTable { public ExceptionHandler exceptionHandlers[]; //newExceptionTable()函数把class文件中的异常处理表转换成 ExceptionTable类型。有一点需要特别说明:异常处理项的catchType 有可能是0。 //我们知道0是无效的常量池索引,但是在这里0并非表 示catch-none,而是表示catch-all,它的用法马上就会看到。 //getCatchType()函数从运行时常量池中查找类符号引用 public ExceptionTable(ExceptionTableEntry entries[], JConstantPool cp) { exceptionHandlers = new ExceptionHandler[entries.length]; for (int i = 0; i < entries.length; i++) { ExceptionTableEntry entry = entries[i]; exceptionHandlers[i] = new ExceptionHandler( entry.StartPc().Value(), entry.EndPc().Value(), entry.HandlerPc().Value(), getCatchType(entry.CatchType().Value(), cp)); } } //getCatchType()函数从运行时常量池中查找类符号引用 public ClassRef getCatchType(int index, JConstantPool cp) { if (index == 0) { return null; // catch all } return (ClassRef) cp.GetConstant(index); } } public class ExceptionHandler { public int startPc ; public int endPc ; public int handlerPc ; public ClassRef catchType ; public ExceptionHandler(int startPc, int endPc, int handlerPc, ClassRef catchType) { this.startPc = startPc; this.endPc = endPc; this.handlerPc = handlerPc; this.catchType = catchType; } }
ExceptionHandler对象中,handlePc就是target的值,而catchType就是type对应的值。最终异常表中的值存储到ExceptionHandler中,那如果抛出异常时,怎样找到ExceptionHandler呢?
//异常处理表查找逻辑前面已经描述过,此处不再赘述。这里注 意两点。第一,startPc给出的是try{}语句块的第一条指令, //endPc给 出的则是try{}语句块的下一条指令。第二,如果catchType是nil(在 class文件中是0),表示可以处理所有异常,这是用来实现finally子句的。 // 第一,startPc给出的是try{}语句块的第一条指令,endPc给 出的则是try{}语句块的下一条指令。 // 第二,如果catchType是nil(在 class文件中是0),表示可以处理所有异常,这是用来实现finally子 句的。 public ExceptionHandler findExceptionHandler(JClass exClass, int pc) { for (ExceptionHandler handler : this.exceptionHandlers) { // jvms: The start_pc is inclusive and end_pc is exclusive if (pc >= handler.startPc && pc < handler.endPc) { if (handler.catchType == null) { return handler; } JClass catchClass = handler.catchType.symRef.ResolvedClass(); if (catchClass == exClass || catchClass.IsSuperClassOf(exClass)) { return handler; } } } return null; }
获取ExceptionHandler的代码如上,遍历所有的exceptionHandlers,判断当前pc是否在exceptionHandler
的处理范围,如果在exceptionHandler的范围之内,则判断当前异常是否是handler的catchType或catchType是当前抛出异常的子异常,如果是,则返回异常处理器,如果遍历到的异常处理器的catchType==null,则表示当前handler能处理所有的异常。这是用来实现finally子 句的。
第六章就到这里了,后面我们再通过例子来分析整个代码的运行逻辑,当然,我写得没有作者那样详细,感兴趣的小伙伴可以去阅读原书,肯定会有更多的收获。
细心的读者肯定发现了问题,我们之前的所有章节中,都只是讲单个方法的运行,好像从来没有讲过方法与方法之间的相互调用,而这在Java开发中再常见不过了。而第7章中主要讲方法与方法之间的相互调用。
在Java 7之前,Java虚拟机规范一共提供了4条方法调用指令,其中invokestatic指令用来调用静态方法,invokespecial指令用来调用无须动态绑定的实例方法,包括构造函数,私有方法和通过super关键字调用的超类方法,剩下的情况则属于动态绑定,如果是针对接口类型的引用调用方法,就使用invokeinterface指令,否则使用invokevirtual指令。
首先,方法调用指令需要n + 1 个操作数,其中第1个操作数是uint16索引,在字节码中紧跟在指令操作码的后面,通过这个索引,可以从当前类的运行时常量池中找一个方法符号引用,解析这个符号引用就可以得到一个方法,注意,这个方法并不一定就是最终要调用的那个方法,所以可能还需要一个查找过程才能找到最终要调用的方法,剩下的n个操作就是要传递给被调用方法的参数,从操作数栈中弹出。
如果要执行的是一个Java方法(而非本地方法),下一步就是给这个方法创建一个新的帧,并把它推到Java虚拟机栈顶,传递参数之后,新的方法就可以开始执行了。
func (self *INVOKE_XXX) Execute(frame *rtda.Frame) {
cp := frame.Method().Class().ConstantPool()
methodRef := cp.GetConstant(self.Index).(*heap.MethodRef)
resolved := resolveMethodRef(methodRef)
checkResolvedMethod(resolved)
toBeInvoked := findMethodToInvoke(methodRef)
newFrame := frame.Thread().NewFrame(toBeInvoked)
frame.Thread().PushFrame(newFrame)
passArgs(frame, newFrame)
}
方法的最后一条指令是某个返回指令,这个指令负责把方法的返回值推入前一帧的操作数栈顶,然后把当前的帧从Java虚拟机栈中弹出。
在MethodRef添加ResolvedMethod()方法,先来看代码 。
public JMethod ResolvedMethod() { if (this.method == null) { this.resolveMethodRef(); } return this.method; } // jvms8 5.4.3.3 public void resolveMethodRef() { JClass d = this.memberRef.symRef.cp.jClass; JClass c = this.memberRef.symRef.ResolvedClass(); //如果类D想通过方法符号引用访问类C的某个方法,先要解析 // 符号引用得到类C。如果C是接口,则抛出 IncompatibleClassChangeError异常 if( c.IsInterface() ){ ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } // 如果找不到对应的方法,则抛出NoSuchMethodError异常,否 则检查类D是否有权限访问该方法。如果没有,则抛出 IllegalAccessError异常。 // isAccessibleTo()方法是在ClassMember结构 体中定义的,在第6章就已经实现了。 JMethod method = lookupMethod(c, this.memberRef.name, this.memberRef.descriptor); if (method == null) { ExceptionUtils.throwException("java.lang.NoSuchMethodError"); } if( !method.classMember.isAccessibleTo(d)) { ExceptionUtils.throwException("java.lang.IllegalAccessError"); } this.method = method; } JMethod lookupMethod(JClass jClass,String name,String descriptor ) { JMethod method = MethodLookup.LookupMethodInClass(jClass, name, descriptor); if (method == null){ method = MethodLookup.lookupMethodInInterfaces(jClass.interfaces, name, descriptor); } return method; } public static JMethod LookupMethodInClass(JClass jClass, String name, String descriptor) { JClass c = jClass; if(c == null){ return null; } while (true) { if(c.methods !=null){ for (JMethod method : c.methods) { if (method.classMember.name.equals(name) && method.classMember.descriptor.equals(descriptor)) { return method; } } } c = c.superClass; if (c == null) { return null; } } } public static JMethod lookupMethodInInterfaces(JClass[] ifaces, String name, String descriptor) { if (ifaces != null) { for (JClass iface : ifaces) { if (iface.methods != null) { for (JMethod method : iface.methods) { if (method.classMember.name.equals(name) && method.classMember.descriptor.equals(descriptor)) { return method; } } } JMethod method = lookupMethodInInterfaces(iface.interfaces, name, descriptor); if (method != null) { return method; } } } return null; }
代码写了那么多,实际上原理很简单,先在当前类中寻找方法,如果找不到,则到父类中递归寻找,如果还找不到,则到当前接口中递归寻找,直到找到为止,如果找不到,则抛出异常。
public JMethod ResolvedInterfaceMethod() { if (this.method == null) { this.resolveInterfaceMethodRef(); } return this.method; } // jvms8 5.4.3.4 public void resolveInterfaceMethodRef() { JClass d = this.memberRef.symRef.cp.jClass; JClass c = this.memberRef.symRef.ResolvedClass(); if (!c.IsInterface()) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } JMethod method = lookupInterfaceMethod(c, this.memberRef.name, this.memberRef.descriptor); if (method == null) { ExceptionUtils.throwException("java.lang.NoSuchMethodError"); } if (!method.classMember.isAccessibleTo(d)) { ExceptionUtils.throwException("java.lang.IllegalAccessError"); } this.method = method; } //如果能在接口中找到方法,就返回找到的方法,否则调用 lookupMethodInInterfaces()函数在超接口中寻找。 public JMethod lookupInterfaceMethod(JClass iface, String name, String descriptor) { for (JMethod method : iface.methods) { if (method.classMember.name.equals(name) && method.classMember.descriptor.equals(descriptor)) { return method; } } return MethodLookup.lookupMethodInInterfaces(iface.interfaces, name, descriptor); }
接口方法符号引用,直接到当前接口中查找,如果查找不到,则到父接口中查找,直到找到为止,如果找不到,抛出java.lang.NoSuchMethodError异常。
在定位需要调用的方法之后,Java虚拟机要给这个方法创建一个新的帧并把它推入Java虚拟机栈顶,然后传递参数。下面来看InvokeMethod()方法 。
public static void InvokeMethod(Frame invokerFrame, JMethod method) { JThread thread = invokerFrame.Thread(); Frame newFrame = thread.NewFrame(method); thread.PushFrame(newFrame); //第一,long和double类型的参数要占用两个位置。 int argSlotCount = method.ArgSlotCount(); if (argSlotCount > 0) { for (int i = argSlotCount - 1; i >= 0; i--) { //第二,对于实例方法,Java编译器会在参数列表的 前面添加一个参数,这个隐藏的参数就是this引用。假设实际的参 //数占据n个位置,依次把这n个变量从调用者的操作数栈中弹出,放 进被调用方法的局部变量表中,参数传递就完成了。 Slot slot = invokerFrame.OperandStack().PopSlot(); //注意,在代码中,并没有对long和double类型做特别处理。因为 操作的是Slot结构体,所以这是没问题的。 newFrame.LocalVars().SetSlot(i, slot); } } }
如果忽略了long和double类型的参数,则静态方法的参数传递过程如下所示 。
方法执行完毕之后,需要把结果返回给调用方,这一工作由返回指令完成,返回指令属于控制类指令,一共有6条,其中return指令用于没有返回值的情况 ,areturn ,ireturn , lreturn,freturn 和dreturn分别用于返回引用,int,long,float和double类型的值。先来看看return指令
// Return void from method public class RETURN extends NoOperandsInstruction { @Override public void Execute(Frame frame) { frame.Thread().PopFrame(); } }
其他的5条指令和Excute()方法都非常相似,这里就不再赘述,就只看看areturn的具体实现。
// Return reference from method public class ARETURN extends NoOperandsInstruction { @Override public void Execute(Frame frame) { JThread thread = frame.Thread(); Frame currentFrame= thread.PopFrame(); Frame invokerFrame = thread.TopFrame(); JObject ref = currentFrame.OperandStack().PopRef(); invokerFrame.OperandStack().PushRef(ref); } }
我们到这里依然来讲之前没有解析的指令,在书中写道,“invokestatic指令用来调用静态方法”,那下面我们就来写一个例子。来看invokestatic指令的使用。
public class FibonacciTest { public static void main(String[] args) { long x = fibonacci(3); System.out.println(x); } private static long fibonacci(long n) { if (n <= 1) { return n; } else { return fibonacci(n - 1) + fibonacci(n - 2); } } }
main方法的字节码指令如下,其中用到了invokestatic和getstatic,现在就来分析invokestatic和getstatic的实现。
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: ldc2_w #2 // long 3l 3: invokestatic #4 // Method fibonacci:(J)J 6: lstore_1 7: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 10: lload_1 11: invokevirtual #6 // Method java/io/PrintStream.println:(J)V 14: return LineNumberTable: line 6: 0 line 7: 7 line 8: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 args [Ljava/lang/String; 7 8 1 x J MethodParameters: Name Flags args
下面来看INVOKE_STATIC指令的代码实现。
// Invoke a class (static) method public class INVOKE_STATIC extends Index16Instruction { @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); MethodRef methodRef =(MethodRef) cp.GetConstant(this.Index); JMethod resolvedMethod = methodRef.ResolvedMethod(); //假定解析符号引用后得到方法M。M必须是静态方法,否则抛 出Incompatible-ClassChangeError异常 if (!resolvedMethod.classMember.IsStatic()) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } JClass jClass = resolvedMethod.classMember.Class(); //先判 断类的初始化是否已经开始,如果还没有,则需要调用类的初始化 方法,并终止指令执行。但是由于此时指令已经执行到了一半, //也 就是说当前帧的nextPC字段已经指向下一条指令,所以需要修改 nextPC,让它重新指向当前指令。Frame结构体的RevertNextPC()方 法做了这样的操作 if (!jClass.InitStarted() ){ frame.RevertNextPC(); ClassInitLogic.InitClass(frame.Thread(), jClass); return; } MethodInvokeLogic.InvokeMethod(frame, resolvedMethod); } }
InvokeMethod()之前已经分析过,如果是静态方法,则第0位不是this,直接将当前操作数栈中的Slot作为参数传入被调用方法本地变量表中。
ttps://img-blog.csdnimg.cn/5649c37f2a014af4ab59e40a8d85dace.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAcXV5aXhpYW8=,size_20,color_FFFFFF,t_70,g_se,x_16)
下面来看字节码的执行流程。
com/test/ch07/FibonacciTest.main() #0 LDC2_W {Index=2} com/test/ch07/FibonacciTest.main() #3 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.main() #3 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.fibonacci() #0 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #1 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #2 LCMP {} com/test/ch07/FibonacciTest.fibonacci() #3 IFGT {Offset=5} com/test/ch07/FibonacciTest.fibonacci() #8 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #9 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #10 LSUB {} com/test/ch07/FibonacciTest.fibonacci() #11 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.fibonacci() #0 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #1 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #2 LCMP {} com/test/ch07/FibonacciTest.fibonacci() #3 IFGT {Offset=5} com/test/ch07/FibonacciTest.fibonacci() #8 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #9 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #10 LSUB {} com/test/ch07/FibonacciTest.fibonacci() #11 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.fibonacci() #0 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #1 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #2 LCMP {} com/test/ch07/FibonacciTest.fibonacci() #3 IFGT {Offset=5} com/test/ch07/FibonacciTest.fibonacci() #6 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #7 LRETURN {} com/test/ch07/FibonacciTest.fibonacci() #14 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #15 LDC2_W {Index=7} com/test/ch07/FibonacciTest.fibonacci() #18 LSUB {} com/test/ch07/FibonacciTest.fibonacci() #19 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.fibonacci() #0 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #1 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #2 LCMP {} com/test/ch07/FibonacciTest.fibonacci() #3 IFGT {Offset=5} com/test/ch07/FibonacciTest.fibonacci() #6 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #7 LRETURN {} com/test/ch07/FibonacciTest.fibonacci() #22 LADD {} com/test/ch07/FibonacciTest.fibonacci() #23 LRETURN {} com/test/ch07/FibonacciTest.fibonacci() #14 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #15 LDC2_W {Index=7} com/test/ch07/FibonacciTest.fibonacci() #18 LSUB {} com/test/ch07/FibonacciTest.fibonacci() #19 INVOKE_STATIC {Index=4} com/test/ch07/FibonacciTest.fibonacci() #0 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #1 LCONST_1 {} com/test/ch07/FibonacciTest.fibonacci() #2 LCMP {} com/test/ch07/FibonacciTest.fibonacci() #3 IFGT {Offset=5} com/test/ch07/FibonacciTest.fibonacci() #6 LLOAD_0 {} com/test/ch07/FibonacciTest.fibonacci() #7 LRETURN {} com/test/ch07/FibonacciTest.fibonacci() #22 LADD {} com/test/ch07/FibonacciTest.fibonacci() #23 LRETURN {} com/test/ch07/FibonacciTest.main() #6 LSTORE_1 {} com/test/ch07/FibonacciTest.main() #7 GET_STATIC {Index=5} com/test/ch07/FibonacciTest.main() #10 LLOAD_1 {} com/test/ch07/FibonacciTest.main() #11 INVOKE_VIRTUAL {Index=6}
为什么会执行两条INVOKE_STATIC指令呢?看RevertNextPC()方法,因为第一次执行时,com/test/ch07/FibonacciTest类并没有初始化,因此需要先初始化,但线程执行不能中断并且要恢复成原来的样子,只此只能调用RevertNextPC()回滚pc的位置。这个在后面再来分析。
接下来,我们来看GET_STATIC指令的代码实现。在FibonacciTest的测试中,并没有使用static属性啊,怎么会去调用GET_STATIC指令呢?System.out.println(x); 这一行,看到没有。System.out,实际上out就是一个静态变量。再来看out的定义 public final static PrintStream out = null ,是System类中PrintStream对象 。属性的descriptor实际上是一个对象类型,GET_STATIC指令的执行实际原理是,从FieldRef中获取当前属性所在的类,调用类的StaticVars()方法,从类中获取所有的静态变量,再判断当前属性在静态变量的位置( int slotId = field.SlotId() 也就是这一行),然后从类的静态变量中取出当前属性的变量值,再根据当前发展的类型调用操作数栈中不同的方法(pushRef()方法),将当前静态变量的值推入操作数栈顶。
// Get static field from class public class GET_STATIC extends Index16Instruction { @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); FieldRef fieldRef = (FieldRef) cp.GetConstant(this.Index); JField field = fieldRef.ResolvedField(); JClass jClass = field.classMember.Class(); //如果声明字段的类还没有初始 化好,也需要先初始化。 if (!jClass.InitStarted() ){ frame.RevertNextPC(); ClassInitLogic.InitClass(frame.Thread(), jClass); return; } if (!field.classMember.IsStatic()) { //如果解析后的字段不是静态字段,也要抛出 IncompatibleClassChangeError异常 ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } String descriptor = field.classMember.Descriptor(); int slotId = field.SlotId(); Slots slots = jClass.StaticVars(); OperandStack stack = frame.OperandStack(); char[] c = descriptor.toCharArray(); switch (c[0]) { case 'Z': case 'B': case 'C': case 'S': case 'I': stack.PushInt(slots.GetInt(slotId)); break; case 'F': stack.PushFloat(slots.GetFloat(slotId)); break; case 'J': stack.PushLong(slots.GetLong(slotId)); break; case 'D': stack.PushDouble(slots.GetDouble(slotId)); break; case 'L': case '[': stack.PushRef(slots.GetRef(slotId)); break; default: } } }
看完GET_STATIC的实现原理,再来看GET_FIELD的实现。
public class GET_FIELD extends Index16Instruction { @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); FieldRef fieldRef = (FieldRef) cp.GetConstant(this.Index); JField field = fieldRef.ResolvedField(); if (field.classMember.IsStatic()) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } OperandStack stack = frame.OperandStack(); JObject ref = stack.PopRef(); if (ref == null) { ExceptionUtils.throwException("java.lang.NullPointerException"); } //弹出对象引用,如果是null,则抛出NullPointerException String descriptor = field.classMember.Descriptor(); int slotId = field.SlotId(); Slots slots = ref.Fields(); char[] c = descriptor.toCharArray(); //根据字段类型,获取相应的实例变量值,然后推入操作数栈。 至此,getfield指令也解释完毕了 switch (c[0]) { case 'Z': case 'B': case 'C': case 'S': case 'I': stack.PushInt(slots.GetInt(slotId)); break; case 'F': stack.PushFloat(slots.GetFloat(slotId)); break; case 'J': stack.PushLong(slots.GetLong(slotId)); break; case 'D': stack.PushDouble(slots.GetDouble(slotId)); break; case 'L': case '[': stack.PushRef(slots.GetRef(slotId)); break; default: } } }
聪明的读者有没有发现GET_STATIC指令和GET_FIELD指令的区别了,有人肯定会发现,GET_STATIC的数据是从当前中的静态变量中获取,而GET_FIELD的值是从当前操作数栈顶中获取,如果当前操作数栈顶中的值为空,则抛出空指针异常,否则获取当前操作数栈顶的对象的Field,根据类型调用不同的操作方法将Field的值推入操作数栈顶。
接下来,我们来看invokespecial指令的实现,invokespecial指令用来调用无须动态绑定的实例方法,包括构造函数,私有方法和通过super关键字调用的超类方法。首先invokespecial指令设计的意图是什么,第一,私有方法和构造函数不需要动态绑定,所以invokespecial指令可以加快方法的调用速度,第二,使用super关键字调用超类中的方法不能使用invokevirtual指令,否则会陷入无限循环 。
在看INVOKE_SPECIAL的实现原理之前,我们还是先来看一个例子。
public class DemoA { public void foo() { System.out.println("A.foo"); } } public class DemoB extends DemoA { } public class DemoC extends DemoB{ public void foo() { super.foo(); } } public class SuperDemo { public static void main(String[] args) { new DemoC().foo(); } }
这个例子很简单,DemoC=> DemoB=>DemoA ,我们来看看字节码的执行流程。
com/test/ch07/testsuper/SuperDemo.main() #0 NEW {Index=2} com/test/ch07/testsuper/SuperDemo.main() #0 NEW {Index=2} com/test/ch07/testsuper/SuperDemo.main() #3 DUP {} com/test/ch07/testsuper/SuperDemo.main() #4 INVOKE_SPECIAL {Index=3} com/test/ch07/testsuper/DemoC.() #0 ALOAD_0 {} com/test/ch07/testsuper/DemoC. () #1 INVOKE_SPECIAL {Index=1} com/test/ch07/testsuper/DemoB. () #0 ALOAD_0 {} com/test/ch07/testsuper/DemoB. () #1 INVOKE_SPECIAL {Index=1} com/test/ch07/testsuper/DemoA. () #0 ALOAD_0 {} com/test/ch07/testsuper/DemoA. () #1 INVOKE_SPECIAL {Index=1} java/lang/Object. () #0 RETURN {} com/test/ch07/testsuper/DemoA. () #4 RETURN {} com/test/ch07/testsuper/DemoB. () #4 RETURN {} com/test/ch07/testsuper/DemoC. () #4 RETURN {} com/test/ch07/testsuper/SuperDemo.main() #7 INVOKE_VIRTUAL {Index=4} com/test/ch07/testsuper/DemoC.foo() #0 ALOAD_0 {} com/test/ch07/testsuper/DemoC.foo() #1 INVOKE_SPECIAL {Index=2}
我们先来看看前面4行 。
com/test/ch07/testsuper/SuperDemo.main() #0 NEW {Index=2} com/test/ch07/testsuper/SuperDemo.main() #0 NEW {Index=2} com/test/ch07/testsuper/SuperDemo.main() #3 DUP {} com/test/ch07/testsuper/SuperDemo.main() #4 INVOKE_SPECIAL {Index=3}
这几行什么意思呢?在SuperDemo类中new DemoC,第一次是因为DemoC没有被加载,因此调用了 frame.RevertNextPC();重新执行了一次NEW的指令,DUP指令主要是复制一份操作数栈顶变量,INVOKE_SPECIAL调用的是DemoC的
// Invoke instance method; // special handling for superclass, private, and instance initialization method invocations public class INVOKE_SPECIAL extends Index16Instruction { @Override public void Execute(Frame frame) { JClass currentClass = frame.Method().classMember.Class(); JConstantPool cp = currentClass.ConstantPool(); MethodRef methodRef = (MethodRef) cp.GetConstant(this.Index); JClass resolvedClass = methodRef.memberRef.symRef.ResolvedClass(); JMethod resolvedMethod = methodRef.ResolvedMethod(); if (resolvedMethod.classMember.Name().equals("") && resolvedMethod.classMember.Class() != resolvedClass) { ExceptionUtils.throwException("java.lang.NoSuchMethodError"); } if (resolvedMethod.classMember.IsStatic()) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } JObject ref = frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1); if (ref == null) { ExceptionUtils.throwException("java.lang.NullPointerException"); } if (resolvedMethod.classMember.IsProtected() && resolvedMethod.classMember.Class().IsSuperClassOf(currentClass) && !resolvedMethod.classMember.Class().GetPackageName().equals(currentClass.GetPackageName()) && ref.Class() != currentClass && !ref.Class().IsSubClassOf(currentClass)) { ExceptionUtils.throwException("java.lang.IllegalAccessError"); } JMethod methodToBeInvoked = resolvedMethod; //如果调用的中超类中的函数,但不是构造函数,且当前类的ACC_SUPER标志被设置,需要一个额外的过程查找最终要调用的 //方法;否则前面从方法符号引用中解析出来的方法就是要调用的方法。 if (currentClass.IsSuper() && resolvedClass.IsSuperClassOf(currentClass) && //resolvedClass是否是currentClass的子类 !resolvedMethod.classMember.Name().equals(" ")) { methodToBeInvoked = MethodLookup.LookupMethodInClass(currentClass.SuperClass(), methodRef.memberRef.Name(), methodRef.memberRef.Descriptor()); } // 如果查找过程失败,或者找到的方法是抽象的,抛出 AbstractMethodError异常。 if (methodToBeInvoked == null || methodToBeInvoked.IsAbstract()) { ExceptionUtils.throwException("java.lang.AbstractMethodError"); } MethodInvokeLogic.InvokeMethod(frame, methodToBeInvoked); } }
在SuperDemo 中new DemoC的实现中,此时currentClass类是SuperDemo,resolvedMethod是DemoC的
com/test/ch07/testsuper/DemoC.() #0 ALOAD_0 {} com/test/ch07/testsuper/DemoC. () #1 INVOKE_SPECIAL {Index=1} com/test/ch07/testsuper/DemoB. () #0 ALOAD_0 {} com/test/ch07/testsuper/DemoB. () #1 INVOKE_SPECIAL {Index=1} com/test/ch07/testsuper/DemoA. () #0 ALOAD_0 {} com/test/ch07/testsuper/DemoA. () #1 INVOKE_SPECIAL {Index=1} java/lang/Object. () #0 RETURN {} com/test/ch07/testsuper/DemoA. () #4 RETURN {} com/test/ch07/testsuper/DemoB. () #4 RETURN {} com/test/ch07/testsuper/DemoC. () #4 RETURN {}
上面接着调用了3个INVOKE_SPECIAL方法,在DemoC的
接着看下面的指令。
com/test/ch07/testsuper/SuperDemo.main() #7 INVOKE_VIRTUAL {Index=4} com/test/ch07/testsuper/DemoC.foo() #0 ALOAD_0 {} com/test/ch07/testsuper/DemoC.foo() #1 INVOKE_SPECIAL {Index=2}
第一个INVOKE_VIRTUAL方法,是什么方法被调用呢?事实上是DemoC实例调用了foo()方法。
接下来,又执行了INVOKE_SPECIAL指令,我们打断点看看。
从代码中看到,methodToBeInvoked方法最终竟然是DemoA的foo()方法,那我们再来回顾一下INVOKE_SPECIAL指令的用途,invokespecial指令用来调用无须动态绑定的实例方法,包括构造函数,私有方法和通过super关键字调用的超类方法,在这个例子中,我们看到了构造函数和super关键字调用超类方法都用到了,感兴趣的小伙伴可以自己去尝试一下private方法调用时INVOKE_SPECIAL代码是如何运行的。
我们再来看另外一个例子,关于INVOKE_INTERFACE指令。
在看指令执行流程时,我们来看作者留给我们的另外一个例子。
public interface Vector { public void multiply(double s); } public class Vector2D implements Vector { protected double x; protected double y; public Vector2D() { this(1, 1); } public Vector2D(double x, double y) { this.x = x; this.y = y; } @Override public void multiply(double s) { this.x *= s; this.y *= s; } } public class Vector3D extends Vector2D { protected double z; public Vector3D() { this(1, 1, 1); } public Vector3D(double x, double y, double z) { super(x, y); this.z = z; } @Override public void multiply(double s) { super.multiply(s); this.z *= s; } } public class InvokeInterfaceTest { public static void main(String[] args) { Vector v = new Vector3D(3.1, 3.2, 3.3); v.multiply(3); } }
在这个例子中,multiply方法就是我们的接口方法。
调用接口方法,根据接口方法名和方法描述,从当前操作数栈顶对象所在类以及父类中找到匹配的方法,进行调用 。接下来,看看接口方法调用实现。
// Invoke interface method //invokeinterface 指令用于调用接口方法,它会在运行时搜索由特定的对象所实现的这个接口方法,并找到适合的方法进行调用 //剩下的情况则属于动态绑定。如果是针对 接口类型的引用调用方法,就使用invokeinterface指令 // 当Java虚拟机通过invokevirtual调用方法时, this引用指向某个类(或其子类)的实例。因为类的继承层次是固定 的, // 所以虚拟机可以使用一种叫作vtable(Virtual Method Table)的技 术加速方法查找。但是当通过invokeinterface指令调用接口方法时, //因为this引用可以指向任何实现了该接口的类的实例,所以无法使 用vtable技术。 // 用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method) public class INVOKE_INTERFACE implements Instruction { public int index; public int index1; public int index2; //注意,和其他三条方法调用指令略有不同,在字节码中, invokeinterface指令的操作码后面跟着4字节而非2字节。 public void FetchOperands(BytecodeReader reader) { this.index = reader.ReadUint16().Value(); //前两字节 的含义和其他指令相同,是个uint16运行时常量池索引。 // count 第3字节的 值是给方法传递参数需要的slot数,其含义和给Method结构体定义 的argSlotCount字段相同。 //正如我们所知,这个数是可以根据方法描 述符计算出来的,它的存在仅仅是因为历史原因。 this.index1 = reader.ReadUint8().Value(); this.index2 = reader.ReadUint8().Value(); // must be 0 第4字节是留给 Oracle的某些Java虚拟机实现用的,它的值必须是0。该字节的存在 是为了保证Java虚拟机可以向后兼容。 } //先从运行时常量池中拿到并解析接口方法符号引用,如果解 析后的方法是静态方法或私有方法,则抛出 IncompatibleClassChangeError异常。 //从操作数栈中弹出this引用,如果引用是null,则抛出 NullPointerException异常。 //如果引用所指对象的类没有实现解析出 来的接口,则抛出IncompatibleClassChangeError异常。 //查找最终要调用的方法。如果找不到,或者找到的方法是抽象 的,则抛出Abstract-MethodError异常。 //如果找到的方法不是public, 则抛出IllegalAccessError异常 public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); InterfaceMethodRef methodRef = (InterfaceMethodRef) cp.GetConstant(this.index); JMethod resolvedMethod = methodRef.ResolvedInterfaceMethod(); if (resolvedMethod.classMember.IsStatic() || resolvedMethod.classMember.IsPrivate()) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } JObject ref = frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1); if (ref == null) { ExceptionUtils.throwException("java.lang.NullPointerException"); } if (!ref.Class().IsImplements(methodRef.memberRef.symRef.ResolvedClass())) { ExceptionUtils.throwException("java.lang.IncompatibleClassChangeError"); } JMethod methodToBeInvoked = MethodLookup.LookupMethodInClass(ref.Class(), methodRef.memberRef.Name(), methodRef.memberRef.Descriptor()); if (methodToBeInvoked == null || methodToBeInvoked.IsAbstract()) { ExceptionUtils.throwException("java.lang.AbstractMethodError"); } if (!methodToBeInvoked.classMember.IsPublic()) { ExceptionUtils.throwException("java.lang.IllegalAccessError"); } MethodInvokeLogic.InvokeMethod(frame, methodToBeInvoked); } }
数组一般指数组对象,在不至于引起混淆的情况下,也可能指数组类,在其他情况下,会明确指出是数组类还是数组对象,如果数组的元素是基本数据类型,就把它叫作基本类型的数组,否则数组元素是引用类型的数组,基本类型的数组肯定都是一维数组,如果引用类型的数组的元素也是数组,那它就是多维数组 。
数组在Java虚拟机中是个比较特殊的概念,为什么这么说呢?有下面的几个原因。
首先,数组类和普通类是不同的,普通类从class文件中加载,但是数组类由Java虚拟机在运行时生成,数组的类名是左方括号([) + 数组元素的类型描述符,数组的类型描述符就是类名本身 ,例如 ,int [] 的类名是[I,int[][]的类名就是[[I,Object[]的类名就是[Ljava/lang/Object; String[][]的类名就是[[java/lang/String;等等。
其次,创建数组的方式和创建普通对象的方式不同,普通对象由new 指令创建,然后由构造函数初始化,基本类型的数组由newarray指令创建,引用类型的数组由anewarray指令创建,另外还有一个专门的multianewarray指令用于创建多维数组 。
最后,很显然,数组和普通对象存放的数组也是不同的,普通对象存放的是实例变量,通过putfield和getfield指令存取,数组对象存放的则是数组元素,通过
首先修改ClassLoader中的LoadClass()方法,如果当前类名是以[开头的,则表示当前类是一个数组类型,因此调用下面数组类型的加载方法。
public JClass loadArrayClass(String name) { JClass clazz = new JClass( new Uint16(Constants.ACC_PUBLIC), name, this, true, //因为数组类不需要 初始化,所以把initStarted字段设置成true。 this.LoadClass("java/lang/Object"), //数组类的超类是 java.lang.Object, new JClass[]{ //并且实现了java.lang.Cloneable和java.io.Serializable 接口。 this.LoadClass("java/lang/Cloneable"), this.LoadClass("java/io/Serializable") } ); this.classMap.put(name, clazz); return clazz; }
接下来,就来看和数组加载相关的4条指令,NEW_ARRAY加载基本数据类型,如boolean,byte,char,short,int,long,float,double类型的数组。在看NEW_ARRAY指令实现前,我们还是先来看一个例子。
public class ArrayIntDemo { public static void main(String[] args) { int a [] = new int[10]; } } sun/misc/VM.() #55 RETURN {} com/test/ch08/ArrayIntDemo.main() #0 BIPUSH{val=10} com/test/ch08/ArrayIntDemo.main() #2 NEW_ARRAY{atype=10} com/test/ch08/ArrayIntDemo.main() #4 ASTORE_1 {} com/test/ch08/ArrayIntDemo.main() #5 RETURN {}
在上面例子中,实现原理很简单,只是创建了一个长度为10,int类型的数组,接下来,来看源码实现。
// Create new array public class NEW_ARRAY implements Instruction { //Array Type atype public final static int AT_BOOLEAN = 4; public final static int AT_CHAR = 5; public final static int AT_FLOAT = 6; public final static int AT_DOUBLE = 7; public final static int AT_BYTE = 8; public final static int AT_SHORT = 9; public final static int AT_INT = 10; public final static int AT_LONG = 11; //newarray指令需要两个操作数。第一个操作数是一个uint8整 数,在字节码中紧跟在指令操作码后面,表示要创建哪种类型的数 组。 // Java虚拟机规范把这个操作数叫作atype,并且规定了它的有效 值 ,如 array type 常量 public Uint8 atype; public void FetchOperands(BytecodeReader reader) { this.atype = reader.ReadUint8(); } public void Execute(Frame frame) { OperandStack stack = frame.OperandStack(); //newarray指令的第二个操作数是count,从操作数栈中弹出,表示数组长度。 int count = stack.PopInt(); //如果count小于0,则抛出NegativeArraySizeException异常 if (count < 0) { ExceptionUtils.throwException("java.lang.NegativeArraySizeException"); } //否则 根据atype值使用当前类的类加载器加载数组类,然后创建数组对象并推入操作数栈 JClassLoader classLoader = frame.Method().classMember.Class().Loader(); JClass arrClass = getPrimitiveArrayClass(classLoader, this.atype.Value()); JObject arr = arrClass.NewArray(count); stack.PushRef(arr); } public JClass getPrimitiveArrayClass(JClassLoader loader, int atype) { switch (atype) { case AT_BOOLEAN: return loader.LoadClass("[Z"); case AT_BYTE: return loader.LoadClass("[B"); case AT_CHAR: return loader.LoadClass("[C"); case AT_SHORT: return loader.LoadClass("[S"); case AT_INT: return loader.LoadClass("[I"); case AT_LONG: return loader.LoadClass("[J"); case AT_FLOAT: return loader.LoadClass("[F"); case AT_DOUBLE: return loader.LoadClass("[D"); default: ExceptionUtils.throwException("Invalid atype!"); } return null; } }
从断点中,可以看到atype的Value值是10,通过getPrimitiveArrayClass()方法的执行逻辑得知10对应的基本类型是int,表示当前需要创建的数组类型是int类型,而从栈顶中弹出一个变量,值也为10,表示当前要创建数组的长度,因此最终创建一个长度为10的int类型数组,初始化值都为0 。
而创建数组的方法如下。
//NewArray()方法专门用来创建数组对象 public JObject NewArray(int count ) { //如果类并不是数组类,就调用panic()函数终止程序执行,否则根据数组类型创建数组对象。 if (!this.IsArray() ){ ExceptionUtils.throwException("Not array class: " + this.name); } switch (this.Name() ){ case "[Z": return new JObject(this, new byte[count], null); case "[B": return new JObject(this, new byte[ count], null); case "[C": return new JObject(this,new char[ count], null); case "[S": return new JObject(this, new short[ count], null); case "[I": return new JObject(this, new int [ count], null); case "[J": return new JObject(this, new long [count], null); case "[F": return new JObject(this, new float[ count], null); case "[D": return new JObject(this, new double [ count], null); default: return new JObject(this,new JObject[ count], null); } }
接下来,我们再来看对象类型的数组创建,还是先看一个例子。
public class ArrayStringDemo { public static void main(String[] args) { String a[] = new String[10]; } } // 字节码指令执行如下 com/test/ch08/ArrayStringDemo.main() #0 BIPUSH{val=10} com/test/ch08/ArrayStringDemo.main() #2 ANEW_ARRAY {Index=2} com/test/ch08/ArrayStringDemo.main() #5 ASTORE_1 {} com/test/ch08/ArrayStringDemo.main() #6 RETURN {}
再来看源码实现。
// Create new array of reference public class ANEW_ARRAY extends Index16Instruction { @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); //anewarray指令也需要两个操作数。第一个操作数是uint16索 引,来自字节码。通过这个索引可以从当前类的运行时常量池中找 到一个类符号引用, // 解析这个符号引用就可以得到数组元素的类。 ClassRef classRef = (ClassRef) cp.GetConstant(this.Index); JClass componentClass = classRef.symRef.ResolvedClass(); OperandStack stack = frame.OperandStack(); //第二个操作数是数组长度,从操作数栈中弹出。 int count = stack.PopInt(); if (count < 0) { ExceptionUtils.throwException("java.lang.NegativeArraySizeException"); } JClass arrClass = componentClass.ArrayClass(); //数组元素的类型和数组长度创建引用类型数组 JObject arr = arrClass.NewArray(count); stack.PushRef(arr); } }
和基本类型数组的创建大同小异,唯一的区别就是基本数据类型还需要根据atype类判断是创建哪种基本类型的数组,对于引用类型的数组,这里好了,直接创建数据类型是Object类型的数组即可。
接下来,我们还是来看多维数组的实现。在看代码之前,还是先来看一个例子。
public static void main(String[] args) { int a[][][] = new int[2][3][4]; int b = a.length; } //方法相关的字节码指令 Code: stack=3, locals=3, args_size=1 0: iconst_2 1: iconst_3 2: iconst_4 3: multianewarray #2, 3 // class "[[[I" 7: astore_1 8: aload_1 9: arraylength 10: istore_2 11: return
首先来看multianewarray指令的实现。
// Create new multidimensional array public class MULTI_ANEW_ARRAY implements Instruction { public Uint16 index; public Uint8 dimensions; @Override public void FetchOperands(BytecodeReader reader) { this.index = reader.ReadUint16(); //指令的第一个操作数是个uint16索引,通过这个 索引可以从运行时常量池中找到一个类符号引用,解析这个引用就 可以得到多维数组类。 this.dimensions = reader.ReadUint8(); //第二个操作数是个uint8整数,表示数组维度。 } @Override public void Execute(Frame frame) { JConstantPool cp = frame.Method().classMember.Class().ConstantPool(); ClassRef classRef = (ClassRef) cp.GetConstant(this.index.Value()); JClass arrClass = classRef.symRef.ResolvedClass(); OperandStack stack = frame.OperandStack(); int[] counts = popAndCheckCounts(stack, this.dimensions.Value()); //函数从操作数栈中弹出n个int值,并且确保 它们都大于等于0。如果其中任何一个小于0 JObject arr = newMultiDimensionalArray(counts, arrClass); stack.PushRef(arr); } public int[] popAndCheckCounts(OperandStack stack, int dimensions) { int[] counts = new int[dimensions]; for (int i = dimensions - 1; i >= 0; i--) { counts[i] = stack.PopInt(); if (counts[i] < 0) { ExceptionUtils.throwException("java.lang.NegativeArraySizeException"); } } return counts; } public JObject newMultiDimensionalArray(int[] counts, JClass arrClass) { int count = counts[0]; JObject arr = arrClass.NewArray(count); if (counts.length > 1) { JObject[] refs = arr.Refs(); for (int i = 0; i < refs.length; i++) { int[] countnew = new int[counts.length - 1]; System.arraycopy(counts, 1, countnew, 0, counts.length - 1); //ComponentClass()方法先根据数组类名推测出数组元素类名, 然后用类加载器加载元素类即可 refs[i] = newMultiDimensionalArray(countnew, arrClass.ComponentClass()); } } return arr; } }
就刚刚这个例子而言, int a[][][] = new int[2][3][4]; dimensions表示数组的维度,在这个例子中,dimensions=3,arrClass的类类型是[[[I ,调用popAndCheckCounts()方法的意图是将每一个维度的数组长度存储下来,比如第一维是2,第二维是3,第三维是4 ,那么调用popAndCheckCounts()的返回值就是 counts = [2,3,4] ,再调用newMultiDimensionalArray()方法递归的创建数组,其调用过程就像剥洋葱一样,一层层向里剥,非常的巧妙创建多维数组,因此a[0] 中存储的实际上是一个二维数组,而不是基本类型int常量值,最终结构只能是a=
{
{
{1,2,3,4},
{5,6,7,8},
{10,11,12}
},
{
{13,14,15,16},
{17,18,19,20},
{21,22,23,24 }
}
}
因此a[0] 只能是
{
{1,2,3,4},
{5,6,7,8},
{10,11,12}
},
而a[0][0]则是{1,2,3,4},而a[0][0][0] = 1,这是读者在阅读这段代码时需要注意的。而在newMultiDimensionalArray()方法中,counts.length > 1时,即结束递归调用,这是怎么回事呢?细心的读者肯定会发现,NewArray()方法本身就返回了一个数组,因此当counts的length等于1时,即终止递归即可,而不是counts的length等于0时才终止递归。
接下来来看ArrayLength的实现。
// Get length of array public class ARRAY_LENGTH extends NoOperandsInstruction { @Override public void Execute(Frame frame) { OperandStack stack = frame.OperandStack(); //如果数组引用是null,则需要抛出NullPointerException异常,否 则取数组长度,推入操作数栈顶即可 JObject arrRef = stack.PopRef(); if (arrRef == null) { ExceptionUtils.throwException("java.lang.NullPointerException"); } int arrLen = arrRef.ArrayLength(); stack.PushInt(arrLen); } } public int ArrayLength() { if (this.data instanceof byte[]) { return ((byte[]) this.data).length; } else if (this.data instanceof short[]) { return ((short[]) this.data).length; } else if (this.data instanceof char[]) { return ((char[]) this.data).length; } else if (this.data instanceof int[]) { return ((int[]) this.data).length; } else if (this.data instanceof Uint16[]) { return ((char[]) this.data).length; } else if (this.data instanceof float[]) { return ((float[]) this.data).length; } else if (this.data instanceof long[]) { return ((long[]) this.data).length; } else if (this.data instanceof double[]) { return ((double[]) this.data).length; } else if (this.data instanceof JObject[]) { return ((JObject[]) this.data).length; } else { ExceptionUtils.throwException("Not array!"); } return 0; }
arrayLength的实现就相当的简单了,直接判断当前对象的data是哪种数据类型,如果是数组类型,调用其length方法即可获取数组的长度。
关于数组这一块的实现原理,这里也说完了,感兴趣的读者可以通过程序调试的方式去研究下面示例的实现,那这一章节也就告一段落了。
public class ArrayDemo { public static void main(String[] args) { int[] a1 = new int[10]; // newarray String[] a2 = new String[10]; // anewarray int[][] a3 = new int[10][10]; // multianewarray int x = a1.length; // arraylength a1[0] = 100; // iastore int y = a1[0]; // iaload a2[0] = "abc"; // aastore String s = a2[0]; // aaload } }
之前的章节中一直在实现Java虚拟机的基本功能,我们已经知道了,想要运行Java程序,除了Java虚拟机之外,还需要Java类库配合,Java虚拟机和Java类库一起构成了Java运行时环境,Java类库主要用Java语言编写,一些无法用Java语言实现的方法则使用本地语言编写,这些方法叫做本地方法。
OpenJDK类库中的本地方法是用JNI(Java Native Interface)编写的,但是要让虚拟机支持JNI规范还需要做大量的工作,因此作者就没有实现了。
作者最终通过自己的方式来实现一套本地方法。
在创建方法时,发现是本地方法,修改其Code指令。
public void injectCodeAttribute(String returnType) { this.maxStack = 4; // 因为本地方法帧的 局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals 字段刚好。 this.maxLocals = this.argSlotCount; //至于code字段,也就是本地方法的字节码,第一条指令 都是0xFE,第二条指令则根据函数的返回值选择相应的返回指令。 switch (returnType.toCharArray()[0]) { case 'V': this.code = new byte[]{(byte) 0xfe, (byte) 0xb1}; // return 0xfe = 15 * 16 + 14 break; case 'L': case '[': this.code = new byte[]{(byte) 0xfe, (byte) 0xb0}; // areturn break; case 'D': this.code = new byte[]{(byte) 0xfe, (byte) 0xaf}; // dreturn break; case 'F': this.code = new byte[]{(byte) 0xfe, (byte) 0xae};// freturn break; case 'J': this.code = new byte[]{(byte) 0xfe, (byte) 0xad}; // lreturn break; default: this.code = new byte[]{(byte) 0xfe, (byte) 0xac};// ireturn break; } }
0xfe表示当前指令是INVOKE_NATIVE,而0xb1,0xb0,0xaf,0xae,0xad,0xac 分别对应的是return,areturn,dreturn,freturn,lreturn,ireturn 指令,也就本地方法都委托给了INVOKE_NATIVE指令来实现。
// Invoke native method public class INVOKE_NATIVE extends NoOperandsInstruction { @Override public void Execute(Frame frame) { JMethod method = frame.Method(); String className = method.classMember.Class().Name(); String methodName = method.classMember.Name(); String methodDescriptor = method.classMember.Descriptor(); //根据类名、方法名和方法描述符从本地方法注册表中查找本 地方法实现,如果找不到,则抛出UnsatisfiedLinkError异常 JNativeMethod nativeMethod = JRegistry.FindNativeMethod(className, methodName, methodDescriptor); if (nativeMethod == null) { String methodInfo = className + "." + methodName + methodDescriptor; ExceptionUtils.throwException("java.lang.UnsatisfiedLinkError: " + methodInfo); } //否则直 接调用本地方法 nativeMethod.run(frame); } }
根据类名,方法名,方法描述符调用FindNativeMethod来查找方法。而FindNativeMethod()方法又是如何实现的呢?
public final static MapJNativeMethod>> registry = new HashMap<>(); //FindNativeMethod()方法根据类名、方法名和方法描述符查找 本地方法实现,如果找不到,则返回nil public static JNativeMethod FindNativeMethod(String className, String methodName, String methodDescriptor) { String key = className + "~" + methodName + "~" + methodDescriptor; Class extends JNativeMethod> method = registry.get(key); if (method != null) { try { return method.newInstance(); } catch (Exception e) { e.printStackTrace(); } } if (methodDescriptor.equals("()V")) { if (methodName.equals("registerNatives") || methodName.equals("initIDs")) { return new JEmptyNativeMethod(); } } return null; }
不知道读者看明白没有。实际上我们注册的所有native方法所在类都实现了JNativeMethod接口,而所有的方法所在类则以key = className + “~” + methodName + “~” + methodDescriptor;注册到registry的map集合中。 因此只需要根据类名,方法名,方法描述符即可找到我们注册的native方法所在类,然后调用其接口方法run()即完成了native方法调用 。通过一个静态代码块注册这些native方法所在类即可 。
static { Register("java/io/FileDescriptor", "set", "(I)J", JFileDescriptor.class); Register("java/io/FileOutputStream", "writeBytes", "([BIIZ)V", JFileOSWriteBytes.class); ... 省略 }
JNativeMethod和其他native方法(如JFileDescriptor)所在类的关系如下 。
public interface JNativeMethod { void run(Frame frame); } public class JFileDescriptor implements JNativeMethod { // private static native long set(int d); // (I)J @Override public void run(Frame frame) { Class extends JNativeMethod> x = JFileDescriptor.class; frame.OperandStack().PushLong(0l); } }
因此INVOKE_NATIVE指令的实现就很简单了,如果是native方法,调用INVOKE_NATIVE指令,根据方法所在类名,方法名,方法描述符找到对应的JNativeMethod类,调用其run方法即可完成native方法的调用,如果需要添加native方法,只需要实现JNativeMethod接口,再将类加入到registry 集合中即可。关于native的每个方法的具体实现,这里就不赘述了,感兴趣的小伙伴,可以自行去下载源码下来研究。
接下来,我们来看代码最终版本。
public static void main(String[] args) { Cmd cmd = new Cmd(); cmd.setCpOption("/Users/quyixiao/gitlab/jvm-java/target/test-classes/com/test/ch06"); cmd.setXjreOption("/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre"); cmd.setJclass("MyObject"); new JVM(cmd).start(); } // newJVM()函数创建JVM结构体实例 public JVM(Cmd cmd) { Classpath cp = new Classpath(cmd.getXjreOption(), cmd.getCpOption()); this.cmd = cmd; this.classLoader = new JClassLoader(cp,cmd.verboseInstFlag); this.mainThread = new JThread(); } //start()方法先初始化VM类,然后执行主类的main()方法 public void start() { this.initVM(); this.execMain(); } //initVM()先加载sun.mis.VM类,然后执行其类初始化方法 public void initVM() { JClass vmClass = this.classLoader.LoadClass("sun/misc/VM"); ClassInitLogic.InitClass(this.mainThread, vmClass); Interpreter.interpret(this.mainThread, this.cmd.verboseInstFlag); } // jvms 5.5 public static void InitClass(JThread thread, JClass jClass) { //nitClass()函数先调用StartInit()方法把类的initStarted状态设 置成true以免进入死循环 jClass.StartInit(); //然后调用scheduleClinit()函数准备执行 类的初始化方法 scheduleClinit(thread, jClass); initSuperClass(thread, jClass); } public static void scheduleClinit(JThread thread, JClass jClass) { //类初始化方法没有参数,所以不需要传递参数。 JMethod clinit = jClass.GetClinitMethod(); if (clinit != null) { // execFrame newFrame = thread.NewFrame(clinit); thread.PushFrame(newFrame); } } public static void initSuperClass(JThread thread, JClass jClass) { //注意,这里有意使用了scheduleClinit这个函数名而非 invokeClinit,因为有可能要先执行超类的初始化方法, if (!jClass.IsInterface()) { JClass superClass = jClass.SuperClass(); if (superClass != null && !superClass.InitStarted() ){ //如果超类的初始化还没有开始,就递归调用InitClass()函数执 行超类的初始化方法,这样可以保证超类的初始化方法对应的帧在 //子类上面,使超类初始化方法先于子类执行。 InitClass(thread, superClass); } } } public JMethod GetClinitMethod() { return this.getStaticMethod(" ", "()V"); }
对于代码中JVM的启动,代码阅读起来还是很简单的,先初始化JVM ,JVM的初始化无非加载sun/misc/VM类,而加载VM避免不了调用其
//execMain()方法先加载主类,然后执行其main()方法 public void execMain() { String className = this.cmd.jclass.replaceAll("\\.", "/"); JClass mainClass = this.classLoader.LoadClass(className); JMethod mainMethod = mainClass.GetMainMethod(); if (mainMethod == null) { log.info("Main method not found in class {} ", this.cmd.jclass); return; } //在调 用main()方法之前,需要给它传递args参数,这是通过直接操作局 部变量表实现的。 //createArgsArray()方法把Go的[]string变量转换成 Java的字符串数组,代码是从interpreter.go文件中拷贝过来的 JObject argsArr = this.createArgsArray(); Frame frame = this.mainThread.NewFrame(mainMethod); frame.LocalVars().SetRef(0, argsArr); this.mainThread.PushFrame(frame); Interpreter. interpret(this.mainThread, this.cmd.verboseInstFlag); }
用户代码的执行也很简单,就是找到main方法,创建新的栈帧,再调用interpret()方法,执行方法指令。而createArgsArray()方法,无非将main(String [] args)中的args参数存放到本地变量表中。
//interpret()方法的其余代码先创建一个Thread实例,然后创建 一个帧并把它推入Java虚拟机栈顶,最后执行方法。 public static void interpret(JThread thread, boolean logInst) { try { loop(thread, logInst); } catch (Exception e) { e.printStackTrace(); logFrames(thread); } } //把局部变量表和操作数栈的内容打印出来,以此来观察方法 的执行结果。还剩一个loop()函数 public static void loop(JThread thread, boolean logInst) { BytecodeReader reader = new BytecodeReader(); while (true) { Frame frame = thread.CurrentFrame(); int pc = frame.NextPC(); thread.SetPC(pc); // decode reader.Reset(frame.Method().Code(), pc); int opcode = reader.ReadUint8().Value(); Instruction inst = Factory.NewInstruction(opcode); inst.FetchOperands(reader); frame.SetNextPC(reader.PC()); if (getClassMethodName(frame).equals("sun/misc/VM.() #55")) { flag = true; } if (flag) { logInstruction(frame, inst); } // execute inst.Execute(frame); if (thread.IsStackEmpty()) { break; } } }
loop方法是整个字节码执行的灵魂所在,首先方法里面写了个死循环,只要Frame不为空,不退出循环。
其实我说这么多,还不如读者自己下载代码下来,打断点调试几个测试用例。下面我们就来看一个例子吧。
public class MyObject { public static int staticVar; public int instanceVar; public static void main(String[] args) { int x = 32768; // ldc MyObject myObj = new MyObject(); // new MyObject.staticVar = x; // putstatic x = MyObject.staticVar; // getstatic myObj.instanceVar = x; // putfield x = myObj.instanceVar; // getfield Object obj = myObj; if (obj instanceof MyObject) { // instanceof myObj = (MyObject) obj; // checkcast int i = 1; } } }
Classfile /Users/quyixiao/gitlab/jvm-java/target/test-classes/com/test/ch06/MyObject.class Last modified 2022-1-2; size 696 bytes MD5 checksum a84e69d705ab90e974cf5c92b66cf550 Compiled from "MyObject.java" public class com.test.ch06.MyObject minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #7.#32 // java/lang/Object."":()V #2 = Integer 32768 #3 = Class #33 // com/test/ch06/MyObject #4 = Methodref #3.#32 // com/test/ch06/MyObject." ":()V #5 = Fieldref #3.#34 // com/test/ch06/MyObject.staticVar:I #6 = Fieldref #3.#35 // com/test/ch06/MyObject.instanceVar:I #7 = Class #36 // java/lang/Object #8 = Utf8 staticVar #9 = Utf8 I #10 = Utf8 instanceVar #11 = Utf8 #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 Lcom/test/ch06/MyObject; #18 = Utf8 main #19 = Utf8 ([Ljava/lang/String;)V #20 = Utf8 args #21 = Utf8 [Ljava/lang/String; #22 = Utf8 x #23 = Utf8 myObj #24 = Utf8 obj #25 = Utf8 Ljava/lang/Object; #26 = Utf8 StackMapTable #27 = Class #33 // com/test/ch06/MyObject #28 = Class #36 // java/lang/Object #29 = Utf8 MethodParameters #30 = Utf8 SourceFile #31 = Utf8 MyObject.java #32 = NameAndType #11:#12 // " ":()V #33 = Utf8 com/test/ch06/MyObject #34 = NameAndType #8:#9 // staticVar:I #35 = NameAndType #10:#9 // instanceVar:I #36 = Utf8 java/lang/Object { public static int staticVar; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public int instanceVar; descriptor: I flags: ACC_PUBLIC public com.test.ch06.MyObject(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/test/ch06/MyObject; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: ldc #2 // int 32768 2: istore_1 3: new #3 // class com/test/ch06/MyObject 6: dup 7: invokespecial #4 // Method " ":()V 10: astore_2 11: iload_1 12: putstatic #5 // Field staticVar:I 15: getstatic #5 // Field staticVar:I 18: istore_1 19: aload_2 20: iload_1 21: putfield #6 // Field instanceVar:I 24: aload_2 25: getfield #6 // Field instanceVar:I 28: istore_1 29: aload_2 30: astore_3 31: aload_3 32: instanceof #3 // class com/test/ch06/MyObject 35: ifeq 46 38: aload_3 39: checkcast #3 // class com/test/ch06/MyObject 42: astore_2 43: iconst_1 44: istore 4 46: return LineNumberTable: line 9: 0 line 10: 3 line 11: 11 line 12: 15 line 13: 19 line 14: 24 line 15: 29 line 16: 31 line 17: 38 line 19: 43 line 21: 46 LocalVariableTable: Start Length Slot Name Signature 0 47 0 args [Ljava/lang/String; 3 44 1 x I 11 36 2 myObj Lcom/test/ch06/MyObject; 31 16 3 obj Ljava/lang/Object; StackMapTable: number_of_entries = 1 frame_type = 254 /* append */ offset_delta = 46 locals = [ int, class com/test/ch06/MyObject, class java/lang/Object ] MethodParameters: Name Flags args } SourceFile: "MyObject.java"
这个例子的字节码执行如下。
com/test/ch06/MyObject.main() #0 LDC {Index=2} com/test/ch06/MyObject.main() #2 ISTORE_1 {} com/test/ch06/MyObject.main() #3 NEW {Index=3} com/test/ch06/MyObject.main() #3 NEW {Index=3} com/test/ch06/MyObject.main() #6 DUP {} com/test/ch06/MyObject.main() #7 INVOKE_SPECIAL {Index=4} com/test/ch06/MyObject.() #0 ALOAD_0 {} com/test/ch06/MyObject. () #1 INVOKE_SPECIAL {Index=1} java/lang/Object. () #0 RETURN {} com/test/ch06/MyObject. () #4 RETURN {} com/test/ch06/MyObject.main() #10 ASTORE_2 {} com/test/ch06/MyObject.main() #11 ILOAD_1 {} com/test/ch06/MyObject.main() #12 PUT_STATIC {Index=5} com/test/ch06/MyObject.main() #15 GET_STATIC {Index=5} com/test/ch06/MyObject.main() #18 ISTORE_1 {} com/test/ch06/MyObject.main() #19 ALOAD_2 {} com/test/ch06/MyObject.main() #20 ILOAD_1 {} com/test/ch06/MyObject.main() #21 PUT_FIELD {Index=6} com/test/ch06/MyObject.main() #24 ALOAD_2 {} com/test/ch06/MyObject.main() #25 GET_FIELD {Index=6} com/test/ch06/MyObject.main() #28 ISTORE_1 {} com/test/ch06/MyObject.main() #29 ALOAD_2 {} com/test/ch06/MyObject.main() #30 ASTORE_3 {} com/test/ch06/MyObject.main() #31 ALOAD_3 {} com/test/ch06/MyObject.main() #32 INSTANCE_OF {Index=3} com/test/ch06/MyObject.main() #35 IFEQ {Offset=11} com/test/ch06/MyObject.main() #38 ALOAD_3 {} com/test/ch06/MyObject.main() #39 CHECK_CAST {Index=3} com/test/ch06/MyObject.main() #42 ASTORE_2 {} com/test/ch06/MyObject.main() #43 ICONST_1 {} com/test/ch06/MyObject.main() #44 ISTORE {Index=4} com/test/ch06/MyObject.main() #46 RETURN {}
#0 LDC {Index=2} :将常量池索引为2的变量存储到操作数栈顶,而此例中,常量池索引为2的是32768
#2 ISTORE_1 {} :将操作数栈顶的元素存储到局部变量表的第1个变量槽位置
#3 NEW {Index=3} : 从常量池中取出索引对3的类名符号,创建新的类,也就是取出com/test/ch06/MyObject创建MyObject对象
#3 NEW {Index=3}:第一次NEW时,MyObject没有被初始化,因此先初始化MyObject,再重新执行NEW指令。将新建的对象推入操作数栈顶。
#6 DUP {} :复制操作数栈顶对象,也就是复制一份MyObject,并重新推入操作数栈顶。
#7 INVOKE_SPECIAL {Index=4}:调用MyObject的
#0 ALOAD_0 {} :加载this到新的帧的栈顶
#1 INVOKE_SPECIAL {Index=1}:调用MyObject父类Object的init方法,并创建新的栈帧。
#0 RETURN {} :Object的init方法中没有其他指令,直接调用RETURN指令,弹出栈帧。
#4 RETURN {} :调用MyObject的init方法中的RETURN指令,弹出栈帧,此时回到了MyObject的main方法。
#10 ASTORE_2 {} : 将当前操作数栈顶的MyObject对象引用存储到局部变量表中第2个变量槽的位置
#11 ILOAD_1 {} :加载局部变量表第一个变量槽的常量32768到操作数栈顶。
#12 PUT_STATIC {Index=5}:从操作数栈中弹出32768赋值给com/test/ch06/MyObject的staticVar静态变量 。
#15 GET_STATIC {Index=5}:取出com/test/ch06/MyObject的staticVar静态变量推入操作数栈顶。
#18 ISTORE_1 {} :将操作数栈顶中的值(32768)存储到局部变量表的第一个变量槽位置 。
#19 ALOAD_2 {} :加载局部变量表第二个变量槽的变量(MyObject)推入操作数栈顶。
#20 ILOAD_1 {} :加载局部变量表第一个变量(32768)推入操作数栈顶。
#21 PUT_FIELD {Index=6}:常量池中index=6 是com/test/ch06/MyObject.instanceVar:I,设置为MyObject对象的instanceVar属性值为32768
#24 ALOAD_2 {} :加载局部变量表第二个变量槽的变量(MyObject对象)推入操作数栈顶。
#25 GET_FIELD {Index=6}:获取操作数栈顶对象的(MyObject)的属性(常量池第6个位置索引的属性)也就是获取MyObject的instanceVar属性推入操作数栈顶。
#28 ISTORE_1 {} :将操作数栈顶元素 存入局部变量表第一个位置的变量槽,也就是将MyObject.instanceVar值覆盖掉x的值。
#29 ALOAD_2 {} :加载局部变量表第二个变量槽的变量推入操作数栈顶,也就是把MyObject对象推入操作数栈顶。
#30 ASTORE_3 {} :再将操作数栈顶的变量存入到本地变量表第三个变量槽的位置 。
#31 ALOAD_3 {} :加载本地变量表第三个变量槽的位置推入操作数栈顶
#32 INSTANCE_OF {Index=3}:判断当前操作数栈顶的元素是否是常量池索引为3的类符号引用的子类 ,如果是,则将1存入操作数栈顶,否则将0存入操作数栈顶。显然MyObject instanceof MyObject 。此时操作数栈顶元素为1 。
#35 IFEQ {Offset=11}:当前操作数栈顶元素是否为0,如果为0,则pc = pc + offset 也就是35 + 11 = 46,直接调用return指令返回。否则继续执行
#38 ALOAD_3 {} :加载局部变量表第3个位置的变量槽的变量推入操作数栈顶,也就是将MyObject对象推入操作数栈顶。
#39 CHECK_CAST {Index=3}:将操作数栈顶的变量强制类型转化为常量池索引为3的类,也就是说将MyObject强制类型转化为MyObject,并推入操作数栈顶。
#42 ASTORE_2 {} : 将操作数栈顶变量(MyObject)存入局部变量表第2个变量槽的位置。也就是覆盖myObj
#43 ICONST_1 {} :将常量1推入操作数栈顶。
#44 ISTORE {Index=4}:将操作数栈顶的变量1存入局部变量表第4个变量槽的位置 。
#46 RETURN {} :弹出当前栈帧。
到这里,终于对《自己动手写Java虚拟机 (Java核心技术系列)》解读完毕,本博客中所有代码都是照作者的go语言版本搬过来的,没有太大的改动。我觉得作者的实战能力,编码能力,编码思路都非常值得我们学习,有兴趣可以去学习一段时间的go语言,而去看作者的书,我相信也会有不少收获。如果不想去学习go语言,用我这个版本的的代码,我相信你也能学习到其他JVM书籍里看不到的东西,话不多说了,还是感谢作者张秀宏让我对Java 虚拟机有了另一种认识,写这篇博客,也是希望有更多的人能看到你的书籍并学习到知识。进一步提升自己的能力,话不多说了,有缘江湖再见。
《自己动手写Java虚拟机》的go语言版本的源码
https://github.com/quyixiao/go_learn.git
《自己动手写Java虚拟机》Java版本的源码
https://github.com/quyixiao/jvm-java.git