【JVM】一、从class文件开始
一、从class文件开始
Java程序的运行包括两个重要的阶段
- 编译阶段:检查源代码是否符合 Java 语法,符合则生成正常的 .class 文件
- 源代码
.java
文件 ——》字节码.class
文件 - 字节码不是存粹的二进制,无法在操作系统中直接执行,在JVM中执行
- 源代码
- 运行阶段:可以在其他操作系统中跨平台执行
1、手撕字节码文件
一个简单的Java源文件
package jvmClass;
public class jvmClass{
private String name;
public String getName() {
return name;
}
}
javac
编译后得到字节码文件
class文件概述
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。
(11 00 在class文件中,11 00 进行存储。高位在前,低位在后。 有一些其他的操作系统的存储方式,低位在前,高位在后,存储成 00 11.)
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
字节码文件结构
字节码文件结构中存在两种数据类型
(1)无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
(2)表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表
魔数与次主版本号
(1)魔数
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。
CAFEBABE(咖啡宝贝)。
(2)次主版本号
JDK版本号
1.1 常量池计数器
字节码文件中,魔数和版本号之后是常量池入口。
概述:
常量池可以比喻为class文件里的资源仓库,是class文件结构中于其他项目关联最多的数据。通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中的常量的数量是不固定的,所以需要在常量池入口处放置一项u2类型的数据,代表常量池容量计数值。
class文件中常量计数器从1开始,而不是从0开始
与Java中语言习惯不同,这个容量计数是从1而不是0开始的。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。
例子1 :Object类没有父类,他的父类索引指向哪里呢?指向 00 00 (指向常量池里的第 0 个常量,第0个常量什么都没有,这个第 0个,就是为了给所有无法指向的情况提供的一个空常量指向)
例子2: 匿名内部类。 (类名称指向哪里?指向 00 00)
1.2 常量池
常量池主要存放两大类常量:
- 字面量(Literal)
- 符号引用(Symbolic References):类加载中会存在解析的过程,即符号引用——>直接引用
字面量:
看上去是啥就存储啥
比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
符号引用
则属于编译原理方面的概念,主要包括下面几类常量:
·被模块导出或者开放的包(Package)
·类和接口的全限定名(Fully Qualified Name)
·字段的名称和描述符(Descriptor)
·方法的名称和描述符
·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址(类加载说。你的虚拟机不运行,你的类就是无用的。一切都要基于jvm运行的时候,类才有他的意义),也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池的项目类型
常量池的项目类型的结构
(1)CONSTANT_Class_info型常量的结构
(2)CONSTANT_Utf8_info型常量的结构
1.2.1 Jvm是如何索引类的?
即jvm是如何确定类的符号引用的?或者说是如何寻找到类的名称的
当jvm运行过程中,如果需要加载额外的类时,
首先会通过对当前类的一个引用(索引),去指向常量池中的class info类型,由class info类型指向utf8 info类型,找到类的全限定名,
在解析的过程中,将全限定名的符号引用转为直接引用。
找到类也就找到了方法。
1.3 访问标志
常量池结束后,是2个字节的访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。
包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否被声明为final等等
类或接口访问标志表
1.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。
类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
1.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量(字段可以理解为变量名)
Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
1.5.1 类变量和实例变量有啥区别
类变量是一个static的变量
class Test{
private static String name;
}
实例变量,非静态的,全局的变量
class Test{
private String name;
}
字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
access_flags:访问标志
字段访问标志(access_flages)
name_index:字段的简单名称的引用(当前字段的名称)
descriptor_index:字段和方法的描述符(当前字段的类型)
描述符含义
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。
用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”
attributes_count:属性表属性的数量
1.6 方法表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项
方法里面的代码去哪里了?
方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。
1.7 属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足表中所定义的结构。
1.7.1 Code属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如表
-
attribute_name_index:是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称
-
attribute_length:指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。
-
max_stack:代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度(stackOverflow 异常)。
-
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放。
注意:并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。
例子:变量槽
publicvoidtest Method() {
private String a = '123';
private String b = '456'
for(int i = 0; i < 0; i++) {
System.out.println(i);
System.out.println(a);
}
for(int x = 0; x < 0; x++) {
System.out.println(x);
System.out.println(a);
}
}
上述代码中有是个局部变量:a, b(String), i和x(int 类型),直观上看至少分配4个变量槽
但是实际只分配两个就行了,因为第一个循环使用了i和a;第二个循环执行的时候,就超出了变量i和a的作用域,此时可以 x 可以重用 i 的变量槽,b 可以重用 a 变量的变量槽。
根据同时生存的最大局部变量的数量和类型计算max_locals的大小。
- code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那顾名思义每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。
关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2的32次幂,但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
1.7.2 ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值(提前赋值,不需要运行时赋值,加快运行时的获取速度)。
只有被static关键字修饰的变量(类变量)才可以使用这项属性。类似“int x=123”和“static int x=123”这样的变量定义在Java程序里面是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>()
方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>()
方法中或者使用ConstantValue属性。
目前Oracle公司实现的Javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>()
方法中进行初始化。
从数据结构中可以看出ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info和CONSTANT_String_info常量中的一种。
1.8 jvm字节码指令
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。举个例子,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立的操作码。
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确指明操作类型的字母,例如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,例如无条件跳转指令goto则是与数据类型无关的指令。
1.8.1 加载和存储
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括:
-
将一个局部变量加载到操作栈:iload、iload_
、lload、lload_ 、fload、fload_ 、dload、dload_ 、aload、aload_ -
将一个数值从操作数栈存储到局部变量表:istore、istore_
、lstore、lstore_ 、fstore、fstore_ 、dstore、dstore_ 、astore、astore_ -
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_
、fconst_ 、dconst_ -
扩充局部变量表的访问索引的指令:wide
1.8.2 运算
-
加法指令:iadd、ladd、fadd、dadd
-
减法指令:isub、lsub、fsub、dsub
-
乘法指令:imul、lmul、fmul、dmul
-
除法指令:idiv、ldiv、fdiv、ddiv
-
求余指令:irem、lrem、frem、drem
-
取反指令:ineg、lneg、fneg、dneg
-
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
-
按位或指令:ior、lor
-
按位与指令:iand、land
-
按位异或指令:ixor、lxor
-
局部变量自增指令:iinc
-
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
1.8.3 类型转换指令
Java虚拟机直接支持(即转换时无须显式的转换指令)以下数值类型的宽化类型转换(Widening Numeric Conversion,即小范围类型向大范围类型的安全转换):
- int类型到long、float或者double类型
- long类型到float、double类型
- float类型到double类型
与之相对的,处理窄化类型转换(Narrowing Numeric Conversion)时,就必须显式地使用转换指令来完成,这些转换指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。
1.8.4 对象创建与访问指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令包括:
-
创建类实例的指令:new
-
创建数组的指令:newarray、anewarray、multianewarray
-
访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic
-
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
-
将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
-
取数组长度的指令:arraylength
-
检查类实例类型的指令:instanceof、checkcast
1.8.5 方法调用和返回指令
方法调用,这里仅列举以下五条指令用于方法调用:
-
invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
-
invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
-
invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法(init)、私有方法(private)和父类方法(父类方调用)。
-
invokestatic指令:用于调用类静态方法(static方法)。
-
invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
1.9 得到的字节码文件
CA FE BA BE //魔数(咖啡宝贝)
00 00 次版本
00 3C 主版本 60
//常量池入口
00 13 常量池常量个数 19个(真正有18个)
//第1个常量
0A 表示方法类型 CONSTANT_Methoder_info
00 04 指向第4个常量 java/lang/Object
// 一旦有一个类需要初始化,必先初始化其父类
00 0F 指向第15个常量 CONSTANT_NameAndType_info
//第2个常量
09 字段的符号引用 CONSTANT_Fieldref_info
00 03 指向第3个常量 jvmClass/jvmClass
00 10 指向第16个常量 CONSTANT_NameAndType_info
//第3个常量 jvmClass/jvmClass
07 类或接口的符号引用 CONSTANT_Class_info
00 11 指向第17个常量
//第4个常量 java/lang/Object
07 类或接口的符号引用 CONSTANT_Class_info
00 12 指向第18个常量
//第5个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 04 length:字符串长度为4个字节
6E 61 6D 65 代表"name"
//第6个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 12 length:字符串长度为18个直接
4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 代表"Ljava/lang/string;"
//L:代表后边是类描述符
//以分号结尾
//第7个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 06 长度为6
3C 69 6E 69 74 3E //"init"方法,Java文件编译为class文件jvm自动生成
//第8个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 03
28 29 56 //"()V" 方法的返回值为void(init方法的返回值)
//第9个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 04
43 6F 64 65 //"Code"属性(方法中都有Code属性)
//第10个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 0F //15
4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 //"LineNumberTable" 代码的行号,为显示错误的行数
//第11个常量
01
00 07
67 65 74 4E 61 6D 65 //"getName" 方法名称
//第12个常量
01
00 14
28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B //"java/lang/string;"
//第13个常量
01
00 0A
53 6F 75 72 63 65 46 69 6C 65 //"SourceFile" 源文件标识
//第14个常量
01
00 0D
6A 76 6D 43 6C 61 73 73 2E 6A 61 76 61 //"jvmClass.java"真正的文件名称
//第15个常量(有一个常量,代表了一个init名称和void名称)
0C 字段或方法的部分符号引用 CONSTANT_NameAndType_info
00 07 指向第7个常量——init
00 08 指向第8个常量——void返回值
//第16个常量(有一个常量,名称叫name,类型是Ljava/lang/string))
0C 字段或方法的符号引用 CONSTANT_NameAndType_info
00 05 指向第5个常量——name
00 06 指向第6个常量——Ljava/lang/string
//第17个常量
01 UTF-8编码的字符串 CONSTANT_Utf8_info
00 11
6A 76 6D 43 6C 61 73 73 2F 6A 76 6D 43 6C 61 73 73 //"jvmClass/jvmClass" 全包名 + 类名
//第18个常量
01
00 10
6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 //"Ljava/lang/Object"
//访问标志
00 21 // ACC_PUBLIC + ACC_SUPER
//类索引
00 03 指向第3个常量 jvmClass
//父类索引
00 04 指向第4个常量 Object
//接口索引
00 00 没有实现任何接口
//字段表计数器
00 01 1个
//第1个字段 ===> private String name;
00 02 private
00 05 指向第5个常量——name
00 06 指向第6个常量——Ljava/lang/string
00 00 没有其他属性
//方法计数器
00 02 //两个方法(init, getName)
//第1个方法
00 01 public
00 07 指向第7个常量——init
00 08 指向第8个常量——void返回值
00 01 有一个属性(Code)
00 09 指向第9个常量——>Code
00 00 00 1D 属性的长度为29
00 01 最大栈深度
00 01 最大局部变量表槽的数量
00 00 00 05 代码编译后所生成的jvm指令码长度code_length
2A //aload_0指令,将第一个属性压入栈顶
B7 //invokespecial,调用所引用对象的构造方法
00 01 //调用第一个方法(init)
B1 //return
00 00 异常信息为空
00 01 Code属性有一个属性
00 0A 指向第10个属性——>"LineNumberTable" 代码的行号
00 00 00 06 属性的长度为6
00 01 有一个属性
00 00 字节码的行号
00 03 代码的行号
//第2个方法
00 01 public
00 0B 指向第11个常量——>getName
00 0C 指向第12个常量——>java/lang/string;
00 01 有一个属性
00 09 指向第9个常量——>Code
00 00 00 1D 属性的长度为29
00 01 最大栈深度
00 01 最大局部变量表槽的数量
00 00 00 05 code属性长度为5(指令码长度)
2A //aload_0指令,将第一个属性压入栈顶
B4 //getfield指令,获取name并return
00 02 //指向第2个常量 Ljava/lang/string;name
B0 //areturn,返回name对象的引用
00 00 //异常信息为空
00 01 //code属性有一个属性
00 0A //指向第10个属性——>"LineNumberTable" 代码的行号
00 00 00 06 属性的长度为6
00 01 有一个属性
00 00 字节码的行号
00 07 代码的行号
00 01 属性表计数器——一个属性
00 0D 指向第13个常量——>SourceFile
00 00 00 02 两个长度
00 0E 指向"jvmClass.java"