# jvm_by_go **Repository Path**: coder_chenjun/jvm_by_go ## Basic Information - **Project Name**: jvm_by_go - **Description**: 学习用go语言实现一个简单的jvm - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-11-08 - **Last Updated**: 2021-11-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] # 第一章. 处理命令行参数 ## java命令 jvm没有规定主类名,所以通过命令行参数(选项)来指定。 > java [-options] class [args] > java [-options] -jar jarfile [args] > javaw [-options] class [args] > javaw [-options] -jar jarfile [args] ## java命令选项 java命令选项分为两大类: - 标准选项 :不会轻易变动。比如-version - 非标准选项:可能在后续的jvm实现中,含义有变化。普通非标准选项以-X开头,高级非标准选项以-XX开头。 | 选项 | 用途 | | :------------: | :------------------: | | -version | 查询jdk版本 | | -cp/-classpath | 指定用户类路径 | | -Xms | 设定初始堆大小(memory size) | | -Xmx | 设定最大的堆空间 | | -Xss | 设置线程栈的大小 | # 第二章. 搜索class文件 ## classpath(cp)选项 通过命令行参数classpath指定class位置或者jar、zip文件路径。也可以用系统分隔符指定多个路径(同类型不同类型都可以),java 6开始可以用通配符(*) 指定某个目录下的所有jar文件。classpath参数指定的位置优先级高于环境变量CLASSPATH的设置。 classpath的常见设置: ``` CLASSPATH=.:$JAVA_HOME\lib\tools.jar ``` 从上面的设定可以知道,需要解决以下几个问题: 1. 支持环境变量 2. 支持多个设置(分隔符) 3. 支持jar也支持目录 ## 目标 - 记录下各种路径,以便后续加载其它类使用,比如loadClass(classname ); - 读取某个类文件的字节流数据 ​ ## 抽象化路径表示 > 支持的路径类型: > > - 目录 > - jar或者zip文件 > - 通配符(*) > - 上面三种用分隔符分隔的组合(windows的分隔符为分号,类Unix操作系统为冒号) 整个路径抽象实现采用的是组合设计模式来完成。 ```go type Entry interface { readClass(className string) ([]byte, Entry, error) } ``` Entry类型本身就表示一个路径,所以readClass方法的参数只需要给定类的全称 + class后缀即可。返回的是读取到的class文件字节数组(传递的参数不能加.class后缀,readClass方法的实现中自动添加.class后缀) > //className的值为java/lang/Object Entry接口的4个实现类: 1. DirEntry:表示一个目录 2. ZipEntry:表示Zip或jar文件 3. CompositeEntry:由其它任意Entry组成,是一个组合类型 4. WildcardEntry:通配符,其实就是CompositeEntry。比如/home/java/lib/* 意思就是把lib目录下的所有jar文件添加到此类型中 ```go type DirEntry struct { absDir string //存放目录的绝对路径,比如/home/myjava } //DirEntry类的readClass方法实现 func (self *DirEntry) readClass(className string) ([]byte, Entry, error) { fileName := filepath.Join(self.absDir, className) data, err := ioutil.ReadFile(fileName) return data, self, err } ``` ```go type ZipEntry struct { absPath string // 这里存放的是jar文件的绝对路径,比如/home/java/tools.jar } //核心代码(有省略) func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) { r, err := zip.OpenReader(self.absPath) for _, f := range r.File { if f.Name == className { rc, err := f.Open() data, err := ioutil.ReadAll(rc) return data, self, nil } } return nil, nil, errors.New("class not found: " + className) } ``` CompositeEntry类型既代表组合路径类型也代表通配符类型,所以WildcardEntry不需要额外再创建类。 ```go type CompositeEntry []Entry func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) for _, entry := range self { data, from, err := entry.readClass(className) if err == nil { return data, from, nil } } return nil, nil, errors.New("class not found: " + className) } ``` ## 路径类型(Entry)实例化 下面就是怎么创建出这些类型实例出来了。 ```go func newEntry(path string) Entry { if strings.Contains(path, pathListSeparator) { return newCompositeEntry(path) } if strings.HasSuffix(path, "*") { return newWildcardEntry(path) } if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") || strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") { return newZipEntry(path) } return newDirEntry(path) } //newDirEntry func newDirEntry(path string) *DirEntry { absDir, err := filepath.Abs(path) return &DirEntry{absDir} } //newZipEntry func newZipEntry(path string) *ZipEntry { absPath, err := filepath.Abs(path) return &ZipEntry{absPath} } //newCompositeEntry func newCompositeEntry(pathList string) CompositeEntry { compositeEntry := []Entry{} for _, path := range strings.Split(pathList, pathListSeparator) { entry := newEntry(path) compositeEntry = append(compositeEntry, entry) } return compositeEntry } //newWildcardEntry //在此类的实现中,只实现对路径下的jar文件处理,不考虑子目录以及class文件的处理 //不会递归处理目录 func newWildcardEntry(path string) CompositeEntry { baseDir := path[:len(path)-1] // remove * compositeEntry := []Entry{} walkFn := func(path string, info os.FileInfo, err error) error {...} filepath.Walk(baseDir, walkFn) //walk用来遍历目录下的所有子目录或者文件,warlFn是个回调函数 return compositeEntry } //walkFn walkFn := func(path string, info os.FileInfo, err error) error { if info.IsDir() && path != baseDir { return filepath.SkipDir } if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") { jarEntry := newZipEntry(path) compositeEntry = append(compositeEntry, jarEntry) } return nil } ``` ------------- ## 类路径种类: - 启动类路径(bootstrap classpath) - 扩展类路径(extension classpath) - 用户类路径(user classpath) > 启动类路径默认对应jre/lib目录,java标准库(大部分在rt.jar)位于该路径。 > > 扩展类路径默认对应jre/lib/ext目录。 > > 我们自己实现的类以及第三方类库属于用户类路径,默认值为当前目录 ## Classpath类型 ```go type Classpath struct { bootClasspath Entry extClasspath Entry userClasspath Entry } func Parse(jreOption, cpOption string) *Classpath {...} func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) {... func (self *Classpath) String() string {...} ``` Parse的jreOption确定***启动类路径***与***扩展类路径***的值,cpOption确定***用户类路径***的值 ```go func Parse(jreOption, cpOption string) *Classpath { cp := &Classpath{} cp.parseBootAndExtClasspath(jreOption) cp.parseUserClasspath(cpOption) return cp } ``` ```go func (self *Classpath) parseBootAndExtClasspath(jreOption string) { jreDir := getJreDir(jreOption) // jre/lib/* jreLibPath := filepath.Join(jreDir, "lib", "*") self.bootClasspath = newWildcardEntry(jreLibPath) // jre/lib/ext/* jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") self.extClasspath = newWildcardEntry(jreExtPath) } //jre目录的算法 //1. 如果传递的是有效的目录,就以此目录为准 //2. 如果传递的目录无效或没传递,就看当前目录下的jre目录 //3. 如果当前目录下也没有jre目录就查看环境变量JAVA_HOME的设置 func getJreDir(jreOption string) string { if jreOption != "" && exists(jreOption) { return jreOption } if exists("./jre") { return "./jre" } if jh := os.Getenv("JAVA_HOME"); jh != "" { return filepath.Join(jh, "jre") } panic("Can not find jre folder!") } ``` ```go func (self *Classpath) parseUserClasspath(cpOption string) { if cpOption == "" { cpOption = "." } self.userClasspath = newEntry(cpOption) } ``` ## 读取class文件的字节流 ```go func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) { className = className + ".class" if data, entry, err := self.bootClasspath.readClass(className); err == nil { return data, entry, err } if data, entry, err := self.extClasspath.readClass(className); err == nil { return data, entry, err } return self.userClasspath.readClass(className) } ``` ## 总结 ​ Classpath类像个门面,包含三种类路径,实际读取class文件时交给各个路径类型去读取。整个读取class文件的流程如下: 1. 程序启动,通过程序的命令行参数设定XjreOption与cpOption的值。 2. 调用Classpath的parse方法,给Classpath的三个属性赋值。 1. 依据XjreOption的值以及当前目录下的jre以及java_home环境变量的值得到 2. 分别创建bootstrap与ext为WildcardEntry类型 3. 调用Classpath的readClass方法读取class文件,读取的顺序是:bootClasspath->extClasspath-> userClasspath # 第三章. 解析class文件 平时我们说的栈、堆、方法区、常量池这些更多的指的是“类文件”被加载解析之后的运行时概念,在这一章,我们讲的指一个“二进制数据”的序列。一个抽象的“类文件”。 之所以说是一个抽象的“类文件”,是因为它可以在运行时动态生成,根本没有物理对应的class文件,它也可以是普通的java类或接口经过编译之后产生的物理上存在的class文件。 类文件的基本知识: ``` 1. class文件基本存储单位是字节 2. 数据在class文件中以big-endian的形式存储 3. 虚拟机规范中定义了u1,u2,u4三种数据类型来表示1,2,4字节无符号整数 4. class中有些内容是定长的,有些内容的大小是不确定的,比如表示类的常量数据 5. 表头表项 5.1 表头是一个u2或u4类型的整数,表示表项数据占用的字节数 5.2 表项就是此表的具体数据 ``` ## 3.1. class文件格式含义 class文件可以按照下面的伪代码表示 > ClassFile { > > u4 magic; //定长 4个字节 > > 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]; > > } 为了了解这个class文件的格式,我编写了下面的类 ```java package jvm; public class BlankClass { } ``` 利用jdk自带的javap 查看此类编译之后的class文件,输出如下: ```java Compiled from "BlankClass.java" public class jvm.BlankClass { public jvm.BlankClass(); } ``` 用classpy工具打开看到的结果如下图: ![BlankClass文件的结构](images/classfile03/BlankClass_classFileStructure.png) ### 3.1.1. magic(魔数) 魔数,无特殊意义,标识是个class文件,值固定为0xCAFEBABE(固定4个字节),如果读取的文件,其魔 数不是0xCAFEBABE,那么就表示这个文件不是一个class文件。 ### 3.1.2. minor_version(次版本号) 次版本号只在J2SE 1.2之前用过,从1.2开始基本没用了,值都是0 ,此数据占用2个字节。 ### 3.1.3. major_version(主版本号) 主版本号在J2SE 1.2之前是45,从1.2开始,每次有大的java版本发布,都会加1.下表列出了一些主版本号对应的java版本。 | java版本 | class文件版本号(有主次版本号) | | ----------- | ------------------ | | JDK 1.0.2 | 45.0~45.3 | | JDK 1.1 | 45.0~45.65535 | | J2SE 1.2 | 46.0 | | J2SE 1.3 | 47.0 | | J2SE 1.4 | 48.0 | | Java SE 5.0 | 49.0 | | Java SE 6 | 50.0 | | Java SE 7 | 51.0 | | Java SE 8 | 52.0 | ### 3.1.4. 常量池 可以把常量池中的常量分为两类:字面量(literal)和符号引用(symbolic reference)。字面量包括数字常量和字符串常量,符号引用包括类和接口名、字段和方法信息等。除了字面量,其他常量都是通过索引直接或间接指向CONSTANT_Utf8_info常量 常量池是一个表结构存储机制,表头存放的是常量池的大小,表项存储的是具体的常量数据。这里说常量池其实不太准确,因为常量池是个运行时概念,这里说为常量数据存储更合适一些。关于常量数据有下面几个基本知识点: 1. 常量数据的表头的值,比实际的常量数据大1 2. 常量数据表项访问,其索引是从1开始,0是无效索引,表示不指向任何常量(顶级类Object的表示类名的索引值就为0) 3. CONSTANT_Long_info和CONSTANT_Double_info各占两个位置。也就是说,如果常量池中存在这两种常量, 实际的常量数量比n–1还要少,而且1~n–1的某些数也会变成无效索引 (TODO:没讲清楚) 类文件的常量数据基本情况见下图: ![常量池的基本结构](images/classfile03/constantpool.png) 常量表头的值,只是可以说明常量表项中的常量的个数,但具体如何解析这些常量表项信息是不充分的。比如下面的2个图,第一个表明一个常量项(item)只占5个字节,而第二个常量项占用3个字节,也就是每一个常量项的长度不是定长的。所以想要准确解析,这就需要了解每一个常量表项的格式与结构。 ![constanOne](images/classfile03/constanOne.png) ![constantTwo](images/classfile03/constantTwo.png) 常量表项中的每一个常量从哪里开始,到哪里结束,并没有严格清晰的规定,但每一个常量表项的格式都是固定 > 类型(tag) + 数据 如果知道了某一个常量表项的类型,那么也就确定了这一个表项的结构、大小等信息,这样也就清晰了如何解析这些数据 ,解析的伪代码如下: ```java for(i = 1 ; i < 常量池表头值 ; i++) { uint8 tag = readTag(); //(tag 代表常量表项的类型值, 占一个字节) switch(tag) { case 1: handleFor1(); case 2: handleFor2(); ...... } } ``` 而在jvm规范中,定义了14个类型的tag类型,如下表所示: | 常量项名字与tag值 | 作用 | 常量项结构 | | -------------------------------- | ---------------------------------------- | ---------------------------------------- | | CONSTANT_Class = 7 | 表示类或接口的符号引用 | {u1 tag,u2 name_index} | | CONSTANT_Fieldref = 9 | 表示字段符号引用,class_index指向Constant_Class,name_and_type_index指向CONSTANT_NameAndType。方法引用与接口方法引用是一样的 | {u1 tag,u2 class_index,u2 name_and_type_index} | | CONSTANT_Methodref = 10 | 表示方法符号引用 | {u1 tag,u2 class_index,u2 name_and_type_index} | | CONSTANT_InterfaceMethodref = 11 | 表示接口方法符号引用,比如:SomInf.m() | {u1 tag,u2 class_index,u2 name_and_type_index} | | CONSTANT_String = 8 | 存放的是java.lang.String的字面量,本身并不存放数据,存放的是指向UTF8的索引 | {u1 tag,u2 string_index} | | CONSTANT_Integer = 3 | 存放Int,byte,boolean,short,char | {u1 tag,u4 bytes} | | CONSTANT_Float = 4 | 存放32位浮点数 | {u1 tag,u4 bytes} | | CONSTANT_Long = 5 | 存放64位长整数 | {u1 tag,u4 high_bytes,u4 low_bytes} | | CONSTANT_Double = 6 | 存放64位浮点数 | {u1 tag,u4 high_bytes,u4 low_bytes} | | CONSTANT_NameAndType = 12 | 存放的是字段或方法的名字与描述符索引 | {u1 tag,u2 name_index,u2 descriptor_index} | | CONSTANT_Utf8 = 1 | 存放的是以MUTF-8编码的字符串,比如字段名,字段描述符等数据 | {u1 tag,u2 length,u1 bytes[length]} | | CONSTANT_MethodHandle = 15 | | | | CONSTANT_MethodType = 16 | | | | CONSTANT_InvokeDynamic = 18 | | | 下面以2个具体的例子来说明解析是如何进行的。第一个例子是读取解析类名的例子。假定读取了一个字节,把这个字节转换为uint8类型,如果值为7,依据上面的常量表,我们知道这个值代表的存储的是类的名字信息 ,而存放类的名字信息的这个表项格式是固定的。其结构为 > > tag(类型:1个字节) + 类名的索引( 2个字节) > 所以接下来读取2个字节的数据,并按照Big-endian的格式转换为一个uint16类型的数据。到此为止,常量数据中的这一个表项就读取完毕了,也可以说解析完毕了(没有真正完成,还要依据这个索引到常量池中此索引的位置读取真正的类名字符串数据)。 第二个例子是读取解析此class文件的源代码文件的名字,在上面的javap命令中已经看到class文件是会记录其源代码文件的。解析过程如下:假定解析到了某一个表项了,读取一个字节的数据,转换为uint8 ,假定得到的tag值为1.那么依据上面的常量类型表可以知道,这里表示的是一个Modified UTF8编码的常量数据,接着知道这种类型的常量项,其格式为: > tag(类型:1个字节) + 后续要读取的字节长度( 2个字节) + 数据 > 依据上面的信息,我们知道需要再读取2个字节的数据,以Big-endian的形式转换为uint16 ,假定值为10,这样我们就知道还需要再读取10个字节的数据,这10个字节的数据,才是真正的存放字符串的数据,剩下的工作就是把这10个字节Modified UTF8编码的数据,转换为真正的字符串就可以了。到此为止,这一个表项数据就解析完毕了。依此类推,整个class文件中常量数据的解析都是这样完成的。 #### 常量池类型设计 ```go type ConstantPool []ConstantInfo //相当于一个常量项 type ConstantInfo interface { readInfo(reader *ClassReader) } ``` //解析class字节流数据中的常量池数据 ```go func readConstantPool(reader *ClassReader) ConstantPool { cpCount := int(reader.readUint16()) //确定常量池有多少个常量项 cp := make([]ConstantInfo, cpCount) for i := 1; i < cpCount; i++ { // 注意索引从1开始 cp[i] = readConstantInfo(reader, cp) switch cp[i].(type) { case *ConstantLongInfo, *ConstantDoubleInfo: i++ // 占两个位置 } } return cp } ``` #### 常量项类型及其实例化 ```go type ConstantInfo interface { readInfo(reader *ClassReader) } //此方法被常量池的readConstantPool方法所调用 func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo { tag := reader.readUint8() c := newConstantInfo(tag, cp) c.readInfo(reader) return c } ``` #### 14个常量项类别 ​ 下面列举几个常量项类型设计与数据读取实现 。 // ConstantIntegerInfo 用来存放4个字节以及小于4个字节数据的常量项 ```go type ConstantIntegerInfo struct { val int32 } func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) { bytes := reader.readUint32() self.val = int32(bytes) } ``` //读取utf8(tag值为1的常量项)数据 ```go type ConstantUtf8Info struct { str string } func (self *ConstantUtf8Info) readInfo(reader *ClassReader) { length := uint32(reader.readUint16()) //读取字符串数据长度 bytes := reader.readBytes(length) //读取等长度的数据(这个数据才是真正的字符串数据) self.str = decodeMUTF8(bytes) } ``` ### 3.1.5. access_flags(类访问标志) 是一个16位的位掩码模式数据,意思是这16位中,某一位可能表示此文件是一个类而不是一个接口,某一位表示访问级别是public。 ### 3.1.6. this_class(本类) 这个记录的是类型的完全限定名,只不过把点号换为斜线,比如上面的java类在class文件中记录的类名为jvm/BlankClass,这也被java语言规范称之为二进制名 而在这个位置,并不是直接记录类的二进制名,而是一个占2个字节的数字,此数字的值代表的是常量池的索引。此索引对应的位置,记录的就是类的二进制名这个字符常量。 下图表示记录本类名字的位置,用两个字节记录的数字为2,而2在常量池中的位置的数据结果为`jvm/BlankClass` ![类名的存储情况](images/classfile03/this_class.png) ### 3.1.7. super_class(父类) 紧跟着的父类与本类是一样的,这里不再赘述。 ### 3.1.8. 接口表 ### 3.1.9. 字段表 字段表与方法表类似,区别只是属性表内容不一样。字段表的格式为: ```go field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } ``` ### 3.1.10. 方法表 与字段表类似 ### 3.1.11. 属性表 和常量池类似,各种属性表达的信息也各不相同,因此无法用统一的结构来定义。不同之处在于,常量是由Java虚拟机规范严格定义的,共有14种。但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型。由于这个原因,Java虚拟机规范没有使用tag,而是使用属性名来区别不同的属性。 属性数据放在属性名之后的u1表中,这样Java虚拟机实现就可以跳过自己无法识别的属性。 属性的结构定义如下: ```go attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } ``` #### 属性表类型设计 每一个属性用AttributeInfo类型表示,整个属性表用AttributeInfo[] 表示 ```go type AttributeInfo interface { readInfo(reader *ClassReader) } func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {... func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {...} func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {...} ``` #### 读取属性表数据 ```go func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo { attributesCount := reader.readUint16() attributes := make([]AttributeInfo, attributesCount) for i := range attributes { attributes[i] = readAttribute(reader, cp) } return attributes } func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo { attrNameIndex := reader.readUint16() attrName := cp.getUtf8(attrNameIndex) attrLen := reader.readUint32() attrInfo := newAttributeInfo(attrName, attrLen, cp) attrInfo.readInfo(reader) return attrInfo } ``` #### 实例化属性类型 ```go func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo { switch attrName { case "Code": return &CodeAttribute{cp: cp} case "ConstantValue": return &ConstantValueAttribute{} case "Deprecated": return &DeprecatedAttribute{} case "Exceptions": return &ExceptionsAttribute{} case "LineNumberTable": return &LineNumberTableAttribute{} case "LocalVariableTable": return &LocalVariableTableAttribute{} case "SourceFile": return &SourceFileAttribute{cp: cp} case "Synthetic": return &SyntheticAttribute{} default: return &UnparsedAttribute{attrName, attrLen, nil} } } ``` #### 23种预定义属性 按照用途,23种预定义属性可以分为三组。第一组属性是实现Java虚拟机所必需的,共有5种;第二组属性是Java类库所必需的,共有12种;第三组属性主要提供给工具使用,共有6种。第三组属性是可选的,也就是说可以不出现在class文件中。 如果class文件中存在第三组属性,Java虚拟机实现或者Java类库也是可以利用它们 的,比如使用LineNumberTable属性在异常堆栈中显示行号。 ##### Constantvalue属性 ContantValue属性,会出现在字段信息种,表示字段的值,比如private int f = 5,那么这个Constantvalue记录的就是***5***这个数据(非直接记录,因为***5***会在常量池种,而不再属性里面),此属性是个***定长属性***. Constantvalue属性的结构如下: ```go ConstantValue_attribute { u2 attribute_name_index; //通过这个索引到常量池种得到属性名字的字符串数据 u4 attribute_length; //4个字节长度,其值一定是2,表示此属性的数据占用2个字节的长度 u2 constantvalue_index;//这个值是一个指向常量池的索引,不同类型的字段值,指向不同的常量项类型 } ``` 对应的类型实现如下: ```go type ConstantValueAttribute struct { constantValueIndex uint16 } func (self *ConstantValueAttribute) readInfo(reader *ClassReader) { self.constantValueIndex = reader.readUint16()//固定读取2个字节的数据 } func (self *ConstantValueAttribute) ConstantValueIndex() uint16 { return self.constantValueIndex } ``` ##### Code属性 此属性是个***变长属性*** ,只存在于方法信息中。此属性用来存放方法的字节码(解释器就是解析这些字节码才让方法代码得到执行的)。 ```go 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;//code属性本身自己的属性,比如记录代码行号的属性 attribute_info attributes[attributes_count]; } ``` Code属性的实现 ```go type CodeAttribute struct { cp ConstantPool maxStack uint16 maxLocals uint16 code []byte exceptionTable []*ExceptionTableEntry attributes []AttributeInfo } type ExceptionTableEntry struct { startPc uint16 endPc uint16 handlerPc uint16 catchType uint16 } func (self *CodeAttribute) readInfo(reader *ClassReader) { self.maxStack = reader.readUint16() self.maxLocals = reader.readUint16() codeLength := reader.readUint32() self.code = reader.readBytes(codeLength) self.exceptionTable = readExceptionTable(reader) self.attributes = readAttributes(reader, self.cp) } ``` //读取异常表 ```go func readExceptionTable(reader *ClassReader) []*ExceptionTableEntry { exceptionTableLength := reader.readUint16() exceptionTable := make([]*ExceptionTableEntry, exceptionTableLength) for i := range exceptionTable { exceptionTable[i] = &ExceptionTableEntry{ startPc: reader.readUint16(), endPc: reader.readUint16(), handlerPc: reader.readUint16(), catchType: reader.readUint16(), } } return exceptionTable } ``` ##### Exceptions属性 记录方法跑出的异常。其结构为: ```go Exceptions_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_exceptions; u2 exception_index_table[number_of_exceptions]; } ``` ## 总结 ​ 本章主要是利用Classreader类型读取原生的class文件字节数组,以形成一个经过解析了Classfile类型。下面是其使用三部曲 ```go cp := classpath.Parse(cmd.XjreOption, cmd.cpOption) //得到classPath类型的对象 className := strings.Replace(cmd.class, ".", "/", -1) //把类名转换为运行时表示 cf := loadClass(className, cp) //解析class数据 printClassInfo(cf)//显示解析之后的class数据 ``` ```go func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile { classData, _, err := cp.ReadClass(className) cf, err := classfile.Parse(classData) return cf } ``` ```go func printClassInfo(cf *classfile.ClassFile) { fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion()) fmt.Printf("constants count: %v\n", len(cf.ConstantPool())) fmt.Printf("access flags: 0x%x\n", cf.AccessFlags()) fmt.Printf("this class: %v\n", cf.ClassName()) fmt.Printf("super class: %v\n", cf.SuperClassName()) fmt.Printf("interfaces: %v\n", cf.InterfaceNames()) fmt.Printf("fields count: %v\n", len(cf.Fields())) for _, f := range cf.Fields() { fmt.Printf(" %s\n", f.Name()) } fmt.Printf("methods count: %v\n", len(cf.Methods())) for _, m := range cf.Methods() { fmt.Printf(" %s\n", m.Name()) } } ``` # 4. 运行时数据区 代码的运行,至少是需要栈,以便存放数据,就像上章我们看到的方法的Code属性,就有一个max_stack之类的数据。本章就是构建运行时结构,以便为后续的代码执行打下基础。 ## 概述 在运行Java程序时,Java虚拟机需要使用内存来存放各式各样的数据。Java虚拟机规范把这些内存区域叫作运行时数据区。 运行时数据区可以分为两类:一类是多线程共享的,另一类则是线程私有的。 多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁。 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。 多线程共享的内存区域主要存放两类数据:类数据和类实例(也就是对象)。 对象数据存放在堆(Heap)中,类数据存放在方法区(Method Area)中。堆由垃圾收集器定期清理,所以程序员不需要关心对象空间的释放。 类数据包括字段和方法信息、方法的字节码、运行时常量池,等等。从逻辑上来讲,方法区其实也是堆的一部分。 线程私有的运行时数据区用于辅助执行Java字节码。每个线程都有自己的pc寄存器(Program Counter)和Java虚拟机栈(JVMStack)。 Java虚拟机栈又由栈帧(Stack Frame,后面简称帧)构成,帧中保存方法执行的状态,包括局部变量表(Local Variable)和操作数栈(Operand Stack)等。 在任一时刻,某一线程肯定是在执行某个方法。这个方法叫作该线程的当前方法;执行该方法的帧叫作线程的当前帧;声明该方法的类叫作当前类。 如果当前方法是Java方法,则pc寄存器中存放当前正在执行的Java虚拟机指令的地址,否则,当前方法是本地方法,pc寄存器中的值没有明确定义 ![rtda](images/ch04/rtda.png) ## 数据类型 ![DataType](images/ch04/DataType.png) 上面的java基本类型,在虚拟机层面,基本可以用一个int32类型来代表,一个不够就用两个(比如long和double) 而引用类型的数据存放的是对象的地址,在一个32位的操作系统上,这个地址值可以用一个int32类型的数据保存,如果是一个64位的操作系统,就可以用2个int32类型的数据保存。但引用类型的数据特殊之处是它可以有一个值null,在虚拟机层面int32的值是不能赋值为nil(go语言nil表示java的null),所以不能简单的用int32类型的数据来存放引用类型的指针。这里创建一个类型Object来代表引用类型 ```go type Object struct { } ``` 然后创建一个类型,来统一代表java的所有类型,代码如下: ```go type Slot struct { num int32 //相当于存放java的值类型 ref *Object //相当于存放java的引用类型 } ``` ## 线程私有的运行时数据区 ### 为什么需要? TODO:为什么需要运行时数据区? 局部变量表与操作数栈的大小是编译器编译代码的时候已经确定了。如下图: ![localVarsAndOperandStack](images/ch04/localVarsAndOperandStack.png) ### 结构 ![runtimeStructure](images/ch04/runtimeStructure.png) ### 线程(Thread) ```go type Thread struct { pc int // 当前正在执行的指令 stack *Stack } ``` ```go func NewThread() *Thread { return &Thread{ stack: newStack(1024), } } ``` ### 栈(Stack) ```go type Stack struct { maxSize uint // 栈的最大容量(最大的Frame数量,每个frame的大小不一定相等) size uint //当前栈中有多少帧(Frame) _top *Frame // stack is implemented as linked list } func newStack(maxSize uint) *Stack { return &Stack{ maxSize: maxSize, } } ``` 栈的常用操作 ```go func (self *Stack) push(frame *Frame) { if self.size >= self.maxSize { panic("java.lang.StackOverflowError") } if self._top != nil { frame.lower = self._top } self._top = frame self.size++ } func (self *Stack) pop() *Frame { if self._top == nil { panic("jvm stack is empty!") } top := self._top self._top = top.lower top.lower = nil self.size-- return top } func (self *Stack) top() *Frame { if self._top == nil { panic("jvm stack is empty!") } return self._top } func (self *Stack) isEmpty() bool { return self._top == nil } ``` ### 帧(Frame) ```go type Frame struct { lower *Frame localVars LocalVars operandStack *OperandStack } ``` ```go func newFrame(thread *Thread) *Frame { return &Frame{ thread: thread, localVars: newLocalVars(method.MaxLocals()), operandStack: newOperandStack(method.MaxStack()), } } ``` ### 局部变量表 ```go type LocalVars []Slot func newLocalVars(maxLocals uint) LocalVars { if maxLocals > 0 { return make([]Slot, maxLocals) } return nil } ``` 存放数据与读取数据 ```go //存取32位的相关数据 func (self LocalVars) SetInt(index uint, val int32) { self[index].num = val } func (self LocalVars) GetInt(index uint) int32 { return self[index].num } //下面是存取64位数据 func (self LocalVars) SetLong(index uint, val int64) { self[index].num = int32(val) self[index+1].num = int32(val >> 32) } func (self LocalVars) GetLong(index uint) int64 { low := uint32(self[index].num) high := uint32(self[index+1].num) return int64(high)<<32 | int64(low) } //存取引用类型的数据 func (self LocalVars) SetRef(index uint, ref *Object) { self[index].ref = ref } func (self LocalVars) GetRef(index uint) *Object { return self[index].ref } ``` ### 操作数栈 操作数栈的实现方式和局部变量表类似。注意:操作数栈与栈不是一回事。 ```go type OperandStack struct { size uint //记录的是当前操作数栈的大小。 slots []Slot } ``` # 5. 指令集和解释器 本章的目标是至少能让下面的java代码能运行起来。 ```java public static void main(){ int a = 5; int b = 6; int result = a + b; } ``` 上面的java方法编译之后形成的字节码如下: > ``` > 0: iconst_5 // 往栈中压入int类型的值5 > 1: istore_0 // 从栈中弹出一个值,并存放到局部变量表(localVars)的索引为0的位置 > 2: bipush 6 // 此指令有操作数,其它的指令都没有操作数,有些操作数包含在了指令中, > // 此指令会读取一个字节的数(byte),提升为int类型,并压入栈中 > > 4: istore_1 // 弹栈,存入localVars索引为1的位置 > 5: iload_0 // 从LocalVars的索引0处读取一个值,并压栈 > 6: iload_1 // 从LocalVars的索引1处读取一个值,并压栈 > 7: iadd // 执行求和指令:没有操作数,从栈中弹出2个int类型的值,求和,并把结果压栈 > 8: istore_2 // 弹栈,存入localVars索引为2的位置 > 9: _return // 从方法中返回。 > ``` 整个执行流程以及最后的局部变量表与操作数栈的情况如下 ``` 整个执行流程: ----bytecode len:-----10 -----pc: 0 inst:*constants.ICONST_5 &{{}} -----pc: 1 inst:*stores.ISTORE_0 &{{}} -----pc: 2 inst:*constants.BIPUSH &{6} -----pc: 4 inst:*stores.ISTORE_1 &{{}} -----pc: 5 inst:*loads.ILOAD_0 &{{}} -----pc: 6 inst:*loads.ILOAD_1 &{{}} -----pc: 7 inst:*math.IADD &{{}} -----pc: 8 inst:*stores.ISTORE_2 &{{}} LocalVars:[{5 } {6 } {11 }] //这里输出好像栈中有数据,实际是没有的,因为add指令执行后,栈中是没有数据的,这里只是toString输出(操作数栈的实现没有真正删除) OperandStack:&{0 [{11 } {6 }]} ``` ## 方法的类型 1. java抽象方法 2. java方法 3. 本地方法 ## 指令结构 我自己顶一下术语: > 字节码(bytecode):也可以称之为指令集(instruction set) > > 操作数(opcode) > > 操作码(operand) 指令,其结构是: > 操作码(opcode)+ [操作数(Operand)] 关于指令有以下几个点需要知道 - 操作码只占一个字节,所以操作码最多只有256个。jvm 8版本只用了205个指令 - 有些操作码不需要操作数,比如iadd(用来把操作数栈中的两个整数弹出,进行求和) - 有些操作码自己就带有操作数,比如iload_0,这里的***0***就是局部变量表的索引值 - 局部变量表与操作数栈只存放数据,不记录数据类型,所以指令的会记录类型,比如iadd中的字母i就表示对整数进行求和。 指令结构如下图: ![instructionStructure](images/ch05/instructionStructure.png) ## 指令分类与含义 指令有九大类 - 常量指令: 把常量push到操作数栈,常量一般来自于: - 指令本身 - 操作数 - 常量池 - 加载指令: 从局部变量表里提取数据并放到操作数栈中 - 存储指令 : 与加载指令刚好相反。 ### 指令首字母含义 ![instructionFirstletter](images/ch05/instructionFirstletter.png) ## 指令的解码 指令的核心指令逻辑: ```go opcode := reader.ReadUint8() //利用BytesReader对象读取一个字节,此字节记录着操作码 inst := instructions.NewInstruction(opcode)//利用此操作码,创建合适的指令对象。 inst.FetchOperands(reader)//指令对象本身读取其操作数(可能有,可能无) inst.Execute(frame) //开始执行指令本身。 ``` ### BytecodeReader 字节码这些二进制数据一般存放在class文件表示方法的code属性中,也是以字节流的形式存在着。所以有必要创建一个专门的类型来读取指令。 ```go type BytecodeReader struct { code []byte // 存放某一个Code属性中记录的所有字节码数据 pc int //记录读取到了字节码中的哪个位置,并不严格等价于程序计数器 } ``` 常用方法: ```go //读取一个字节 func (self *BytecodeReader) ReadInt8() int8 { return int8(self.ReadUint8()) } func (self *BytecodeReader) ReadUint8() uint8 { i := self.code[self.pc] self.pc++ return i } //读取2个字节 func (self *BytecodeReader) ReadInt16() int16 { return int16(self.ReadUint16()) } func (self *BytecodeReader) ReadUint16() uint16 { byte1 := uint16(self.ReadUint8()) byte2 := uint16(self.ReadUint8()) return (byte1 << 8) | byte2 } ``` ### 指令类型设计 指令的执行需要借助局部变量表和操作数栈等信息,也就是说指令的执行需要Frame类型,下面是指令类型的设计。 ```go type Instruction interface { FetchOperands(reader *BytecodeReader) //从字节码中提取操作数 Execute(frame *Frame) //执行指令逻辑 } ``` 一些指令不需要操作数,只需要一个操作数,需要2个操作数等,依据依赖操作数数量这种情况可以创建几个抽象父类,然后具体的指令,继承某一个抽象父类完成指令的执行逻辑。下面是列出的几个代表性实现。 ```go type NoOperandsInstruction struct { //无操作数的抽象指令父类 // empty } func (self *NoOperandsInstruction) FetchOperands(reader *BytecodeReader) { // nothing to do } ``` ```go type Index8Instruction struct { //只有一个字节操作数的抽象指令父类 Index uint } func (self *Index8Instruction) FetchOperands(reader *BytecodeReader) { self.Index = uint(reader.ReadUint8()) } ``` ```go type BranchInstruction struct { //只有2个字节操作数的抽象指令父类 Offset int } func (self *BranchInstruction) FetchOperands(reader *BytecodeReader) { self.Offset = int(reader.ReadInt16()) } ``` ## 常见指令的实现 ### iconst_5 这一指令把隐含在操作码中的常量值推入操作数栈顶,注意: 此指令的助记符就是:iconst_5 .其实现如下: ```go type ICONST_5 struct{ base.NoOperandsInstruction } func (self *ICONST_5) Execute(frame *rtda.Frame) { frame.OperandStack().PushInt(5) } ``` ### iload 此指令从局部变量表索引为0处获取数据,然后推入操作数栈顶 ```go //iload_0指令实现 type ILOAD_0 struct{ base.NoOperandsInstruction } func (self *ILOAD_0) Execute(frame *rtda.Frame) { _iload(frame, 0) } //iload_1指令实现 type ILOAD_1 struct{ base.NoOperandsInstruction } func (self *ILOAD_1) Execute(frame *rtda.Frame) { _iload(frame, 1) } func _iload(frame *rtda.Frame, index uint) { val := frame.LocalVars().GetInt(index) frame.OperandStack().PushInt(val) } ``` ### istore_1 ```go type ISTORE_1 struct{ base.NoOperandsInstruction } func (self *ISTORE_1) Execute(frame *rtda.Frame) { _istore(frame, 1) } func _istore(frame *rtda.Frame, index uint) { val := frame.OperandStack().PopInt() frame.LocalVars().SetInt(index, val) } ``` ### bipush bipush指令从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶,此指令也属于常量指令这一类 ```go type BIPUSH struct { val int8 } func (self *BIPUSH) FetchOperands(reader *base.BytecodeReader) { self.val = reader.ReadInt8() } func (self *BIPUSH) Execute(frame *rtda.Frame) { i := int32(self.val) frame.OperandStack().PushInt(i) } ``` ### iadd 这类指令属于算术运算指令。 ```go type IADD struct{ base.NoOperandsInstruction } func (self *IADD) Execute(frame *rtda.Frame) { stack := frame.OperandStack() v2 := stack.PopInt() v1 := stack.PopInt() result := v1 + v2 stack.PushInt(result) } ``` ### _return ```go func (self *RETURN) Execute(frame *rtda.Frame) { //frame.Thread().PopFrame() add := frame.LocalVars().GetInt(2) fmt.Printf("this is _return instruction execution thre sum of 5 + 6 = ------%v \n",add) } ``` ## 指令的解释执行 指令的解释执行的基本流程: 1. 利用方法的属性数据,得到局部变量表与操作数栈的大小 2. 得到字节码数据 3. 创建线程 1. 创建线程栈(Stack) 4. 创建帧 1. 创建局部变量表,并设定最大的slot值 2. 创建操作数栈,并设定最大的slot值 5. 执行指令 1. 读取字节码数据中的一个字节 2. 依据此字节数据创建合适的指令类型 3. 读取指令的操作数 4. 调用指令的execute方法真正执行指令。 5. 设定frame对象的nextPC值 6. 继续读取指令,循环往复,直到指令执行完毕。 下面的代码,只是这一阶段的实现,并不正确的。有以下几个问题 1. 只支持执行一个方法的字节码 2. 没有实现return相关的指令,执行到最后会报错 3. 此时此刻,thread对象的pc还没有用到,注释掉这行代码不影响程序的执行。此时只用到了帧层面的pc。在支持方法调用后,线程的pc值才会真正起作用。 ```go func interpret(methodInfo *classfile.MemberInfo) { codeAttr := methodInfo.CodeAttribute() maxLocals := codeAttr.MaxLocals() maxStack := codeAttr.MaxStack() bytecode := codeAttr.Code() thread := rtda.NewThread() frame := thread.NewFrame(maxLocals, maxStack) thread.PushFrame(frame) defer catchErr(frame) loop(thread, bytecode) } ``` ```go func loop(thread *rtda.Thread, bytecode []byte) { frame := thread.PopFrame() reader := &base.BytecodeReader{} for { pc := frame.NextPC() thread.SetPC(pc) fmt.Print("----bytecode len:-----") fmt.Println(len(bytecode)) // decode reader.Reset(bytecode, pc) opcode := reader.ReadUint8() //pc一直指向最新的位置 inst := instructions.NewInstruction(opcode) inst.FetchOperands(reader) frame.SetNextPC(reader.PC()) // execute fmt.Printf("-----pc:%2d inst:%T %v\n", pc, inst, inst) inst.Execute(frame) } } ``` ------ # 6. 类和对象 上章已经可以让代码可以执行了,但不支持方法的调用,而为了支持方法调用(静态方法与实例方法),首先需要实现类与对象在虚拟机里面的表示。 此章主要是处理类,对象的运行时处理,比如如何加载,用什么内容表示这个加载的class文件以及实例化之后的对象。也会实现线程共享的运行时数据区(包括方法区与运行时常量池) ## 方法区 方法区,它是运行时数据区的一块逻辑区域,由多个线程共享。 方法区主要存放从class文件获取的类信息。此外,类变量也存放在方法区中。当Java虚拟机第一次使用某个类 时,它会搜索类路径,找到相应的class文件,然后读取并解析class文件,把相关信息放进方法区。 至于方法区到底位于何处,是固定大小还是动态调整,是否参与垃圾回收,以及如何在方法区内存放类数据等,Java虚拟机规范并没有明确规定 ![Class_Field_Method_constantPool](images/ch06/Class_Field_Method_constantPool.png) ### 类(Class) ```go type Class struct { accessFlags uint16 name string // 二进制名 superClassName string // 二进制名 interfaceNames []string // 二进制名 constantPool *ConstantPool fields []*Field methods []*Method loader *ClassLoader superClass *Class interfaces []*Class instanceSlotCount uint staticSlotCount uint staticVars Slots } ``` ### 类的加载 #### Classloader ```go type ClassLoader struct { cp *classpath.Classpath classMap map[string]*Class // loaded classes 也可以理解为方法区 } func NewClassLoader(cp *classpath.Classpath) *ClassLoader { return &ClassLoader{ cp: cp, classMap: make(map[string]*Class), } } ``` #### 加载类的流程 整体的流程如下: 1. 实例化Classloader对象 2. 调用Classloader对象的loadClass方法 1. 从Classloader对象的方法区缓存中查找,有就返回,没有就调用loadNonArrayClass方法 1. 调用readClass方法得到class文件字节数组 2. 调用defineClass把字节数组转换为Class类型对象 1. 调用parseClass方法把字节数组转换为Class对象 1. 调用classfile.parse方法 2. 调用classloader.newClass方法 2. 设定class对象的loader 3. 解析父类(调用loadClass方法加载父类,所以有个递归) 4. 解析接口类(调用loadClass方法加载父类,所以有个递归) 5. 放置class到方法区缓存 3. 调用link方法 1. verify方法(校验是否是有效的class文件,这个方法没有实现) 2. prepare方法 1. calcInstanceFieldSlotsID:给每一个实例字段(包含父类)分配一个id 2. calcStaticFieldSlotsID:给每一个静态字段分配一个id 3. allocAndInitStaticVars:初始化staticVars变量,并给static final类型的字段赋值。 ```go //构建Class:把ClassFile对象转换为Class对象(这2个对象都是go语言层面的对象) func newClass(cf *classfile.ClassFile) *Class { class := &Class{} class.accessFlags = cf.AccessFlags() class.name = cf.ClassName() class.superClassName = cf.SuperClassName() class.interfaceNames = cf.InterfaceNames() class.constantPool = newConstantPool(class, cf.ConstantPool()) //常量池也在方法区 class.fields = newFields(class, cf.Fields()) class.methods = newMethods(class, cf.Methods()) return class } ``` ### AccessFlags accessFlags是类的访问标志,总共16比特。字段和方法也有访问标志,但具体标志位的含义可能有所不同 ```go const ( ACC_PUBLIC = 0x0001 // class field method ACC_PRIVATE = 0x0002 // field method ACC_PROTECTED = 0x0004 // field method ACC_STATIC = 0x0008 // field method ACC_FINAL = 0x0010 // class field method ACC_SUPER = 0x0020 // class ACC_SYNCHRONIZED = 0x0020 // method ACC_VOLATILE = 0x0040 // field ACC_BRIDGE = 0x0040 // method ACC_TRANSIENT = 0x0080 // field ACC_VARARGS = 0x0080 // method ACC_NATIVE = 0x0100 // method ACC_INTERFACE = 0x0200 // class ACC_ABSTRACT = 0x0400 // class method ACC_STRICT = 0x0800 // method ACC_SYNTHETIC = 0x1000 // class field method ACC_ANNOTATION = 0x2000 // class ACC_ENUM = 0x4000 // class field ) ``` ### 成员(MemberInfo) ```go type ClassMember struct { accessFlags uint16 name string descriptor string class *Class } func (self *ClassMember) copyMemberInfo(memberInfo *classfile.MemberInfo) { self.accessFlags = memberInfo.AccessFlags() self.name = memberInfo.Name() self.descriptor = memberInfo.Descriptor() } ``` #### Field ```go type Field struct { ClassMember constValueIndex uint slotId uint } func newFields(class *Class, cfFields []*classfile.MemberInfo) []*Field { fields := make([]*Field, len(cfFields)) for i, cfField := range cfFields { fields[i] = &Field{} fields[i].class = class fields[i].copyMemberInfo(cfField) fields[i].copyAttributes(cfField) } return fields } func (self *Field) copyAttributes(cfField *classfile.MemberInfo) { if valAttr := cfField.ConstantValueAttribute(); valAttr != nil { self.constValueIndex = uint(valAttr.ConstantValueIndex()) } } ``` #### Method ```go type Method struct { ClassMember maxStack uint maxLocals uint code []byte } func newMethods(class *Class, cfMethods []*classfile.MemberInfo) []*Method { methods := make([]*Method, len(cfMethods)) for i, cfMethod := range cfMethods { methods[i] = &Method{} methods[i].class = class methods[i].copyMemberInfo(cfMethod) methods[i].copyAttributes(cfMethod) } return methods } func (self *Method) copyAttributes(cfMethod *classfile.MemberInfo) { if codeAttr := cfMethod.CodeAttribute(); codeAttr != nil { self.maxStack = codeAttr.MaxStack() self.maxLocals = codeAttr.MaxLocals() self.code = codeAttr.Code() } } ``` ## 运行时常量池 ***这里的常量池实现核心逻辑:把classfile中的一个个的常量信息(带tag信息)中真正的数据,用一个go语言对象(Object)数组存放起来,因为运行时常量与classfile中的常量的索引是匹配的,以后要使用的时候(getConstant),依据索引可以得到常量值。*** 运行时常量池主要存放两类信息:字面量(literal)和符号引用(symbolic reference)。字面量包括整数、浮点数和字符串字面量.符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用 符号引用包含以下几个类别(不一定只有这几个): - 类符号引用 - 字段符号引用 - 方法符号引用 - 接口方法符号引用 ```go type Constant interface{} type ConstantPool struct { class *Class consts []Constant } ``` ```go func newConstantPool(class *Class, cfCp classfile.ConstantPool) *ConstantPool { cpCount := len(cfCp) consts := make([]Constant, cpCount) rtCp := &ConstantPool{class, consts} for i := 1; i < cpCount; i++ { cpInfo := cfCp[i] switch cpInfo.(type) { case *classfile.ConstantIntegerInfo: intInfo := cpInfo.(*classfile.ConstantIntegerInfo) consts[i] = intInfo.Value() case *classfile.ConstantFloatInfo: floatInfo := cpInfo.(*classfile.ConstantFloatInfo) consts[i] = floatInfo.Value() case *classfile.ConstantLongInfo: longInfo := cpInfo.(*classfile.ConstantLongInfo) consts[i] = longInfo.Value() i++ case *classfile.ConstantDoubleInfo: doubleInfo := cpInfo.(*classfile.ConstantDoubleInfo) consts[i] = doubleInfo.Value() i++ case *classfile.ConstantStringInfo: stringInfo := cpInfo.(*classfile.ConstantStringInfo) consts[i] = stringInfo.String() case *classfile.ConstantClassInfo: classInfo := cpInfo.(*classfile.ConstantClassInfo) consts[i] = newClassRef(rtCp, classInfo) case *classfile.ConstantFieldrefInfo: fieldrefInfo := cpInfo.(*classfile.ConstantFieldrefInfo) consts[i] = newFieldRef(rtCp, fieldrefInfo) case *classfile.ConstantMethodrefInfo: methodrefInfo := cpInfo.(*classfile.ConstantMethodrefInfo) consts[i] = newMethodRef(rtCp, methodrefInfo) case *classfile.ConstantInterfaceMethodrefInfo: methodrefInfo := cpInfo.(*classfile.ConstantInterfaceMethodrefInfo) consts[i] = newInterfaceMethodRef(rtCp, methodrefInfo) default: // todo } } return rtCp } func (self *ConstantPool) GetConstant(index uint) Constant { if c := self.consts[index]; c != nil { return c } panic(fmt.Sprintf("No constants at index %d", index)) } ``` ### 符号引用创建 ![SymbolicReference](images/ch06/SymbolicReference.png) ```go type SymRef struct { cp *ConstantPool className string class *Class //这里是符号引用对应的类。也起到一个缓存的作用。见ResolvedClass } type ClassRef struct { SymRef } //这个方法是classloader中newConstantPool方法调用的时候会被调用。 func newClassRef(cp *ConstantPool, classInfo *classfile.ConstantClassInfo) *ClassRef { ref := &ClassRef{} ref.cp = cp ref.className = classInfo.Name() return ref } ``` 其它三种符号引用 ```go type MemberRef struct { SymRef name string descriptor string } type FieldRef struct { MemberRef field *Field } type MethodRef struct { MemberRef method *Method } type InterfaceMethodRef struct { MemberRef method *Method } ``` ## 对象(Object) ```go type Object struct { class *Class fields Slots } ``` ## 类与对象的相关指令 相关指令有10个,如下: 1. new 2. putstatic与getstatic 3. putfield与getfield 4. instanceOf与checkcast 5. ldc相关指令 ```java 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 } } } ``` ### 符号引用使用 下面通过new指令的实现过程说明符号引用的使用,这个指令主要使用了类符号引用 new指令,操作数占2个字节,值为2,这个值指向的是常量池的索引。: ![newInstruction](images/ch06/newInstruction.png) 类符号引用:name_index值为14,这个14也是指向常量池的索引。 ![ClassSymref](images/ch06/ClassSymref.png) 类符号引用最终的值: ![classSymrefValue](images/ch06/classSymrefValue.png) 下面是代码的实现逻辑 ```go type NEW struct{ base.Index16Instruction }//new指令的操作数占2个字节 func (self *NEW) Execute(frame *rtda.Frame) { cp := frame.Method().Class().ConstantPool() //在这个案例中index值为2,转换为ClassRef类型 classRef := cp.GetConstant(self.Index).(*heap.ClassRef) class := classRef.ResolvedClass() //省略 } ``` 解析类符号引用:先从缓存中找(缓存就是class字段),找不到才解析 ```go func (self *SymRef) ResolvedClass() *Class { if self.class == nil { self.resolveClassRef() } return self.class } ``` 真正的解析过程 ```java class A{ public static void main(){ new B(); } } ``` ```go func (self *SymRef) resolveClassRef() { d := self.cp.class //这里的类是当前常量池关联的类(也就是当前运行方法所在的类) //这里的类指的是A c := d.loader.LoadClass(self.className) //这里是依据类符号引用解析后得到的类。//这里的类指的是B if !c.isAccessibleTo(d) { panic("java.lang.IllegalAccessError") } self.class = c //存放到缓存里。 } ``` ## 总结 本章主要完成的功能有: 1. 加载类 1. 给加载的类进行初始化 2. 给类的常量字段赋值 2. 实例化对象 1. 给实例化的对象进行初始化 3. 实现了如下几个指令(并实现了符号引用的解析) 1. new 2. putstatic与getstatic 3. putfield与getfield 4. instanceOf与checkcast 5. ldc -------- # 7. 方法调用和返回 本章的实现,不考虑jdk 8中接口的静态方法调用与接口中的默认方法调用问题。 ## 方法调用简介 ### 方法分类 从调用的角度来看: ​ 方法可以分为两类:静态方法(或者类方法)和实例方法。静态方法通过类来调用,实例方法则通过对象引用来调用。 ​ 静态方法是静态绑定的,也就是说,最终调用的是哪个方法在编译期就已经确定。 ​ 实例方法则支持动态绑定,最终要调用哪个方法可能要推迟到运行期才能知道 从实现的角度来看: ​ 方法分为三类: - 没有实现(抽象方法) - java语言实现 - 本地语言实现(比如c或c++) ### 方法调用的逻辑 方法调用的核心逻辑: - 依据方法的符号引用,找到被调用的方法 - 创建一个新的帧,以便执行找到的方法 - 把方法调用的参数,传递给新创建出来的帧。 - 被调用方法执行完毕会执行某个return指令,此时把被调用方法的结果存放到调用者帧中的操作数栈中,并把被调用方法的帧从栈中弹出即完成了方法调用的逻辑。 ### 方法调用指令 在java 7之前,虚拟机规范一共提供了4条方法调用指令 1. invokestatic指令调用静态方法 2. invokespecial指令调用无须动态绑定的实例方法(除了这3个,其它都属于动态方法绑定调用) 1. 本类的构造函数 2. 私有方法 3. 通过super关键字调用的超类方法 3. invokeinterface:针对接口类型的引用方法调用 4. invokevirtual ### 方法返回指令 方法执行完毕之后,需要把结果返回给调用方,这一工作由返回指令完成。返回指令属于控制类指令,一共有6条。如果被调用方法有返回值,就把返回值放置到调用者的OperandStack中。 1. return:没有返回值 2. areturn:返回引用 3. ireturn:返回int 4. lreturn:返回long 5. freturn:返回float 6. dreturn:返回double ## 方法调用流程实例分析 ### java代码 通过下面的java代码来分析整个方法调用的流程。 ```java public class Ch07InvokeMethod { public static void main(String[] args) { IA instance = new Aimpl(); instance.m(); } } interface IA{ void m(); } class Aimpl implements IA{ @java.lang.Override public void m() { int a = 100; } } ``` ### class文件 三个类产生的类文件的方法相关的信息: Aimpl类:(3条指令) ![Aimpl](images/ch07/Aimpl.png) IA接口的class文件(无Code属性,无指令) ![IA](images/ch07/IA.png) Ch07InvokeMethod类的(14条指令) ![InvokeMethodWithMain](images/ch07/InvokeMethodWithMain.png) ### 方法符号引用解析 上面main方法中的第九条指令invokeinterface才是调用方法,其中的操作数的值4,就是指向常量池的索引。依据这个索引可以得到方法m的符号引用,情况如下图: ![InvokeMethodConstantInfo](images/ch07/InvokeMethodConstantInfo.png) ### 找出真正的方法 上面是方法符号引用的解析过程,解析完毕后,可以在IA中找到m方法,但接口IA中的m方法是没有代码的,那真正的Aimpl类中的m方法是如何找到的呢?这就要靠invokeinterface指令之前的aload这样的指令,把IA接口的实现对象push到OperandStack中(实例方法的调用,第一个参数总是方法所属对象,这里指的就是Aimpl类的实例,剩下的才是方法的参数)。 invokeinterface指令的实现代码如下: ```go func (self *INVOKE_INTERFACE) FetchOperands(reader *base.BytecodeReader) { self.index = uint(reader.ReadUint16()) reader.ReadUint8() // count reader.ReadUint8() // must be 0 } func (self *INVOKE_INTERFACE) Execute(frame *rtda.Frame) { cp := frame.Method().Class().ConstantPool() methodRef := cp.GetConstant(self.index).(*heap.InterfaceMethodRef) resolvedMethod := methodRef.ResolvedInterfaceMethod() if resolvedMethod.IsStatic() || resolvedMethod.IsPrivate() { panic("java.lang.IncompatibleClassChangeError") } //这里就是取得IA接口的实现类对象 ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1) if ref == nil { panic("java.lang.NullPointerException") // todo } if !ref.Class().IsImplements(methodRef.ResolvedClass()) { panic("java.lang.IncompatibleClassChangeError") } //这里才是找接口实现类的方法,也就是真正要执行的代码。上面查找方法,有点判断接口中有没有这个方法,判断实现类是否真的实现接口的作用? methodToBeInvoked := heap.LookupMethodInClass(ref.Class(), methodRef.Name(), methodRef.Descriptor()) if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() { panic("java.lang.AbstractMethodError") } if !methodToBeInvoked.IsPublic() { panic("java.lang.IllegalAccessError") } base.InvokeMethod(frame, methodToBeInvoked) } ``` ### 创建方法执行的运行时环境 ```go func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) { thread := invokerFrame.Thread() newFrame := thread.NewFrame(method) thread.PushFrame(newFrame) argSlotCount := int(method.ArgSlotCount()) if argSlotCount > 0 { //由于操作的是slot结构体,所以不需要对long,double类型做特殊处理 for i := argSlotCount - 1; i >= 0; i-- { slot := invokerFrame.OperandStack().PopSlot() newFrame.LocalVars().SetSlot(uint(i), slot) } } } ``` ### 参数传递 静态方法调用参数传递 ![staticMethodInvokePassArgs](images/ch07/staticMethodInvokePassArgs.png) 实例方法调用参数传递 ![instanceMethodInvokePassArgs](images/ch07/instanceMethodInvokePassArgs.png) ### 被调用方法返回 下面是没有返回值的return指令的实现 ```go type RETURN struct{ base.NoOperandsInstruction } func (self *RETURN) Execute(frame *rtda.Frame) { frame.Thread().PopFrame() } ``` 下面是返回double类型的dreturn指令的实现。其它返回指令类似 ```go type DRETURN struct{ base.NoOperandsInstruction } func (self *DRETURN) Execute(frame *rtda.Frame) { thread := frame.Thread() currentFrame := thread.PopFrame() invokerFrame := thread.TopFrame() val := currentFrame.OperandStack().PopDouble() invokerFrame.OperandStack().PushDouble(val)//存放被调用方法的返回值 } ``` ## 总结 本章主要完成了以下几个事情 1. 方法调用 2. 类型初始化(静态构造函数调用),见new指令的实现 3. thread pc与frame pc的使用,见new指令的实现(frame的RevertNextPC方法) # 8. 数组和字符串 数组在Java虚拟机中是个比较特殊的概念。 - 普通的类从class文件中加载,但是数组类由Java虚拟机在运行时生成 - 数组的类名是左方括号([)+数组元素的类型描述符。比如:int[]的类名是[I,Object[]的类名是 [Ljava/lang/Object - 普通对象由new指令创建,然后由构造函数初始化。基本类型数组由newarray指令创建;引用类型数组由anewarray指令创建;另外还有一个专门的multianewarray指令用于创建多维数组。 - 普通对象中存放的是实例变量,通过putfield和getfield指令存取。数组对象中存放的则是数组元素,通过aload和astore系列指令按索引存取。其中可以是a、b、c、d、f、i、l或者s,分别用于存取引用、 byte、char、double、float、int、long或short类型的数组 - 数组有一个arraylength指令,用于获取数组长度,普通类型没有这样的指令 ## 数组实现 ​ ### 数组对象 ​ 在没实现数组之前,Object代码是这样的: ```go type Object struct { class *Class fields Slots } ``` 由于数组也是对象,为了让Object类型既可以表示普通的对象,也可以表示数组对象,需要把Object类型改造为下面的样子: ```go type Object struct { class *Class data interface{} } ``` Go语言的interface{}类型很像C语言中的void*,该类型的变量可以容纳任何类型的值.这里的interface{}类型,可以理解为java语言中Object类型。 ### 数组类 创建数组: ```go func (self *Class) NewArray(count uint) *Object { if !self.IsArray() { panic("Not array class: " + self.name) } switch self.Name() { case "[Z": return &Object{self, make([]int8, count)} case "[B": return &Object{self, make([]int8, count)} case "[C": return &Object{self, make([]uint16, count)} case "[S": return &Object{self, make([]int16, count)} case "[I": return &Object{self, make([]int32, count)} case "[J": return &Object{self, make([]int64, count)} case "[F": return &Object{self, make([]float32, count)} case "[D": return &Object{self, make([]float64, count)} default: return &Object{self, make([]*Object, count)} } } ``` 判断是否是数组: ```go func (self *Class) IsArray() bool { return self.name[0] == '[' } ``` ### 加载数组类 ```go func (self *ClassLoader) LoadClass(name string) *Class { if class, ok := self.classMap[name]; ok { return class } if name[0] == '[' { // array class return self.loadArrayClass(name) } return self.loadNonArrayClass(name) } func (self *ClassLoader) loadArrayClass(name string) *Class { class := &Class{ accessFlags: ACC_PUBLIC, // todo name: name, loader: self, initStarted: true,//数组类不需要初始化 //数组类的父类是Object,并且实现了Cloneable与Serializable接口 superClass: self.LoadClass("java/lang/Object"), interfaces: []*Class{ self.LoadClass("java/lang/Cloneable"), self.LoadClass("java/io/Serializable"), }, } self.classMap[name] = class return class } ``` ## 数组相关指令 本节要实现20条指令,其中newarray、anewarray、multianewarray和arraylength指令属于引用类指令;aload和astore系列指令各有8条,分别属于加载类和存储类指令。下面的Java程序演示了部分数组相关指令的用处 ```java 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 } } ``` ## 字符串 在class文件中,字符串是以MUTF8格式保存的。在Java虚拟机运行期间,字符串以java.lang.String(后面简称String)对象的形式存在,而在String对象内部,字符串又是以UTF16格式保存的。字符串相关功能大部分都是由String(和StringBuilder等)类提供的. ### 字符串实现原理 String类有两个实例变量。其中一个是value,类型是字符数组,用于存放UTF16编码后的字符序列。另一个是hash,缓存计字符串的哈希码,代码如下: ```java public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 ... // 其他代码 } ``` 这样可以通过直接把java.lang.String加载进来,然后直接给里面的value[]字段赋值的方式来实现字符串,比如下面的代码: ```go // go string -> java.lang.String func JString(loader *ClassLoader, goStr string) *Object { if internedStr, ok := internedStrings[goStr]; ok { return internedStr } chars := stringToUtf16(goStr) jChars := &Object{loader.LoadClass("[C"), chars} jStr := loader.LoadClass("java/lang/String").NewObject() jStr.SetRefVar("value", "[C", jChars) internedStrings[goStr] = jStr return jStr } ``` ### 字符串池的实现 用map来表示字符串池,key是Go字符串,value是Java字符串 ```go var internedStrings = map[string]*Object{} ``` ```go // java.lang.String -> go string func GoString(jStr *Object) string { charArr := jStr.GetRefVar("value", "[C") return utf16ToString(charArr.Chars()) } ``` ### ldc指令 如果ldc试图从运行时常量池中加载字符串常量,则先通过常量拿到Go字符串,然后把它转成Java字符串实例并把引用推入操作数栈顶。 ```go func _ldc(frame *rtda.Frame, index uint) { stack := frame.OperandStack() class := frame.Method().Class() c := class.ConstantPool().GetConstant(index) switch c.(type) { case int32: stack.PushInt(c.(int32)) case float32: stack.PushFloat(c.(float32)) case string: internedStr := heap.JString(class.Loader(), c.(string)) stack.PushRef(internedStr) ... // 其他代码不变 } ``` ### 类加载器 这里增加了字符串类型静态常量的初始化逻辑。 ```go func initStaticFinalVar(class *Class, field *Field) { vars := class.staticVars cp := class.constantPool cpIndex := field.ConstValueIndex() slotId := field.SlotId() if cpIndex > 0 { switch field.Descriptor() { ... // 其他case语句不变 case "Ljava/lang/String;": goStr := cp.GetConstant(cpIndex).(string) jStr := JString(class.Loader(), goStr) vars.SetRef(slotId, jStr) } } } ``` ## 字符串案例分析 ```java public class Ch08StringTest { public static void main(String[] args){ String a = "abc"; //ldc String b = new String("abc"); //new , ldc } } ``` 生成的class文件中的code如下: ![StringCaseAnalysis](images/ch08/StringCaseAnalysis.png) ldc指令会创建一个String对象,并缓存到字符串池中,代码如下: ```go func _ldc(frame *rtda.Frame, index uint) { stack := frame.OperandStack() class := frame.Method().Class() c := class.ConstantPool().GetConstant(index) //省略××××× case string: internedStr := heap.JString(class.Loader(), c.(string)) stack.PushRef(internedStr) } func JString(loader *ClassLoader, goStr string) *Object { //先从缓存中取 if internedStr, ok := internedStrings[goStr]; ok { return internedStr } chars := stringToUtf16(goStr) jChars := &Object{loader.LoadClass("[C"), chars} //实例化了对象 jStr := loader.LoadClass("java/lang/String").NewObject() jStr.SetRefVar("value", "[C", jChars) internedStrings[goStr] = jStr return jStr } ``` 而new指令会直接创建一个java.lang.String对象。这样就创建了2个String对象了。到PC值为7的ldc指令时,因为前面已经加载过abc这个数据,这里会直接从缓存中取得java.lang.String对象,不会创建新的String对象。所以上面的代码,整个只会创建2个字符串对象。 如果把String a ="abc"去掉,也会创建2个字符串对象。ldc一个,new String一个。 # 总结 本章主要完成了数组类型的加载,加载就是简单的实例化一个Class对象并设定父类与接口就完成了数组类型的加载了。 字符串的处理,就是直接加载java.lang.String并通过给此类中的一个char[]类型的变量value直接赋值的方式来完成字符串的处理。 还有一点需要注意:**常量池与字符串池是不一样的** ----- # 9. 本地方法调用 主要通过实现Object的hashCode方法来演示这个问题。 # 10. 异常处理 ## 异常抛出与本地方法调用 异常抛出牵涉到了本地方法的处理,并不仅仅只是实例化一个异常对象的问题。这个本地方法主要用来处理方法调用栈的问题,见下面的java代码: ```java // java.lang.Throwable public synchronized Throwable fillInStackTrace() { if (stackTrace != null ||backtrace != null /* Out of protocol state */ ) { fillInStackTrace(0); stackTrace = UNASSIGNED_STACK; } return this; } //本地方法 private native Throwable fillInStackTrace(int dummy); ``` ## 异常处理表 ## 异常相关指令 ## 虚拟机栈信息 # 11. 总结篇 # 12. 附录 ## 12.1 大端与小端 对于整型、长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节)而 Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放据的低位字节到高位字节)。 例如,假设从内存地址 0x0000 开始有以下数据: | 地址1 | 地址2 | 地址3 | 地址4 | | ------ | ------ | ------ | ------ | | 0x0000 | 0x0001 | 0x0002 | 0x0003 | | 0x12 | 0x34 | 0xab | 0xcd | 如果我们从地址 0x0000开始读取的四个字节变量,若字节序为big-endian,则读出结果为0x1234abcd;若字节序为little-endian,则读出结果为0xcdab3412。 ## 12.2描述符 1. 类型描述符。 1. 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。注意,long的描述符是J而不是L。 2. 引用类型的描述符是L+类的完全限定名+分号。 3. 数组类型的描述符是[+数组元素类型描述符。 2. 字段描述符就是字段类型的描述符。 3. 方法描述符是(分号分隔的参数类型描述符)+返回值类型描述符,其中void返回值由单个字母V表示 ---------- 下面是一些例子 | 字段描述符 | 字段 | 方法描述符 | 方法 | | ------------------- | ------------------ | --------------------- | ------------------------- | | S | short | ()V | void xx() | | Ljava.lang.Object; | java.lang.Object | ()Ljava.lang.String; | String xx() | | [I] | int[] | (Ljava.lang.String;)V | void xx(String[] args) | | [[D | double二维数组 | (FF)I | int xx(float x,float y) | | [Ljava.lang.String; | java.lang.String[] | ([J;J)I | int xx(Long[] a,long key) | # 13. GO语言快速入门 ## 13.1 go安装与配置 安装: 下载安装或解压,可选的设置环境变量GOROOT值为go的安装目录,可以通过go env命令检查环境变量设置,有IDE时,环境变量的设置都可以不必设置。 go项目目录结构: 工作空间(一个目录) ,没有IDE,那么这个目录设置给go的环境变量GOPATH,在这个工作空间下,添加以下几个目录 - src目录(必须): - pkg目录:存放编译好的包对象文件 - bin目录 ## 13.2. idea配置go的开发环境 1. 到go语言的官方网站下载go的相关安装包,并安装 2. 安装idea的go插件 参考网址: ``` http://blog.csdn.net/skytoup/article/details/50063037 http://studygolang.com/articles/6251 ``` 3. 创建一个go项目 3.1. 创建项目时,会提示设定go sdk的目录(go的安装目录) 3.2. 创建项目时,设定的项目位置为GOPATH的路径,项目名字任意。 4. 设定Run and Configuration 4.1 创建go app build,设定类型为package,表示此包内的所有文件都会得到编译处理 ``` 类型为file,需要指定main函数所在的文件,只有这一个文件会得到编译运行。 ``` 4.2 设定包的名字为包含main函数的文件所在的目录。 5. 直接运行。 5.1 运行时,如果需要指定参数,在运行配置中指定“程序的参数”, ``` 而不是“工具参数”,这里的工具参数指的是go编译程序的运行参数 ``` ## 13.3 go语言 # 参考资料 1. [官方java语言与虚拟机规范(有各个版本的)](http://docs.oracle.com/javase/specs/index.html) 2. [大端与小端](http://www.cnblogs.com/luxiaoxun/archive/2012/09/05/2671697.html) 3. [虚拟机与语言规范](https://docs.oracle.com/javase/specs/index.html) 4. ​