Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文 件),它不面向任何特定的处理器,只面向虚拟机。Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的 问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效, 而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可 在多种不同的计算机上运行。
【1】JVM 的“无关性”
Java具有平台无关性,也就是任何操作系统都能运行Java代码。之所以能实现这一点,是因为Java运行在虚拟机上,不同的操作系统都拥有各自的Java虚拟机,因此Java能实现“一次编写,处处运行”。
而JVM不仅具有平台无关性,还具有语言无关性。平台无关性是指不同的操作系统都有各自的JVM,而语言无关性是指Java虚拟机能运行除Java以外的代码。
Java源代码首先需要使用javac编译器编译成class文件,然后启动JVM执行class文件,从而程序开始运行。
也就是JVM只认识class文件,它并不管何种语言生成了class文件,只要class文件符合JVM的规范就能运行。
因此目前已经有Scala、JRuby、Jython等语言能够在JVM上运行。它们有各自的语法规则,不过它们的编译器都能将各自的源码编译成符合JVM规范的class文件,从而能够借助JVM运行它们。
java中的编译器和解释器
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。
这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。
在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每 一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运 行,这就是上面提到的Java的特点的编译与解释并存的解释。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中 解释器----->机器可执行的二进制机器码---->程序运行。
【2】Class文件结构
class文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全是连续的0/1。Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间地数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
每一个 Class 文件对应于一个如下所示的 ClassFile 结构体。
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]; }
这种数据结构,类似C语言结构体。这个结构体中只有两种数据类型:无符号数和表。
class文件中的所有内容被分为两种类型:无符号数和表。
① 无符号数
它表示class文件中的值,这些值没有任何类型,但有不同的长度。根据这些值长度的不同分为:u1 , u2 ,u4 , u8,分别代表了1字节的无符号数,2字节的无符号数,4字节的无符号数和8字节的无符号数。
无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
② 表
class文件中的所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表。即 class文件中的数据要么是单个值,要么是二维表。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。
③ class文件的组织结构
- 魔数
- 本文件的版本信息
- 常量池
- 访问标志
- 类索引
- 父类索引
- 接口索引集合
- 字段表集合
- 方法表集合
本文中class文件源码如下:
public class ClassA { private int a =10; private static int b=20; public int getA() { return a; } public void setA(int a) { this.a = a; } public static int getB() { return b; } public static void setB(int b) { ClassA.b = b; } }
【3】Class文件的构成1–魔数
class文件的头4个字节称为魔数-u4类型的数据,用来表示这个class文件的类型。
魔数的作用就相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在class文件中标示文件类型比较合适。
Class 文件的魔数的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝),这个魔数值再 Java 还称做“Oak”语言的时候(大约是 1991 年前后)就已经确定下来了。
魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。
【4】Class文件的构成2–版本信息
紧接着魔数之后的4个字节为Java版本信息(它表示本class中使用的是哪个版本的JDK):第5和第6个字节是次版本号(minor_version),第7和第8个字节是主版本号(major_version)。
Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0 ~ 1.1 使用了 45.0 ~ 45.3 的版本号),
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。
例如,JDK 1.1 能支持版本号为 45.0 ~ 45.65535 的 Class 文件,无法执行版本号为 46.0 以上的 Class 文件,而 JDK 1.2 则能支持 45.0 ~ 46.65535 的 Class 文件。 JDK 版本为 1.7,可生成的 Class 文件主版本号最大值为 51.0.
在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM无法运行高版本的class文件。即使该class文件中没有用到任何高版本JDK的特性也无法运行。
下面给出了Java各个主版本号,以供参考。
JDK版本 | 字节码中的主版本号 |
Java 1.2 | 0x002E=46 |
Java 1.3 | 0x002F=47 |
Java 1.4 | 0x0030=48 |
Java 5 | 0x0031=49 |
Java 6 | 0x0032=50 |
Java 7 | 0x0033=51 |
Java 8 | 0x0034=52 |
本例中,使用的为JDK1.8,第78字节为0x0034=52。
【5】Class文件的构成3–常量池
constant_pool_count
常量池计数器,u2类型的数据。它是常量池的入口,表示紧跟着它后面的常量池的元素个数。常量池的大小是不固定的(程序运行期间会动态添加常量),因此常量池的开头放置一个u2类型的无符号数(constant_pool_count),用来存储当前常量池的容量。
JVM根据这个值就知道常量池的头尾(这个值是从1开始,若为5表示池中有四个常量)。如下所示常量池容量(偏移地址:0x00000008)为十六进制数0x001E,即十进制30,这代表常量池中有29项常量,索引值范围为1~29。
Class文件结构中只有常量池的容量计数从1开始,对于其他集合类型,包括接口索引结合,字段表集合,方法表集合等容量技术都与一般习惯相同,从0开始。
这里我用jdk的内置工具javap,反编译一下,可以输出常量池的信息以及元素个数。执行命令:javap -verbose ClassA.class。输出结果如下(也可以使用javap -c ClassA.class):
Constant pool: #1 = Methodref #5.#25 // java/lang/Object."<init>":()V #2 = Fieldref #4.#26 // com/test/classes/ClassA.a:I #3 = Fieldref #4.#27 // com/test/classes/ClassA.b:I #4 = Class #28 // com/test/classes/ClassA #5 = Class #29 // java/lang/Object #6 = Utf8 a #7 = Utf8 I #8 = Utf8 b #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lcom/test/classes/ClassA; #16 = Utf8 getA #17 = Utf8 ()I #18 = Utf8 setA #19 = Utf8 (I)V #20 = Utf8 getB #21 = Utf8 setB #22 = Utf8 <clinit> #23 = Utf8 SourceFile #24 = Utf8 ClassA.java #25 = NameAndType #9:#10 // "<init>":()V #26 = NameAndType #6:#7 // a:I #27 = NameAndType #8:#7 // b:I #28 = Utf8 com/test/classes/ClassA #29 = Utf8 java/lang/Object
可是,我们得到的常量池里的元素个数是29。我们看常量池第一个元素,它的索引是从1开始的。所以索引值范围是1~29。设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。
根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中。这个常量就对应Null值,所以常量池的索引从1而非0开始。
① 什么是常量池
紧接着版本号之后的就是常量池了。一个Java类中定义的很多信息都是由常量池维护和描述的。可以将常量池看作是Class文件的资源库。比如:Java类中定义的方法与变量信息,都是存储在常量池中。
常量池中主要存储两类常量:字面常量和符号引用。字面量,如文本字符串,Java中声明为常量值,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
常量池(constant_pool)是在编译期被确定,并被保存在已编译的class文件中的一些数据。除了包含代码各处中所定义的各种基本类型(如 int、long等)和对象型(如 String 及数组)的常量值外,还包含一些以文本形式出现的符号引用。
(1.1)字面量
字面量即我们在程序中定义的字符串,被final修饰的值和基本类型的值(非包装类型)。
public class A{ int a = 11; // 11 应该存放在哪里?--常量池 static int d=20; // 20 应该存放在哪里?--常量池 String str = "abc"; static final int b = 10; // 字符串“abc” 和整型数值 10 就位于常量池中。 //... }
(1.2)符号引用
符号引用就是我们定义的各种名字:
- 类和接口的全限定名;
- 字段的名字和描述符;
- 方法的名字和描述符。
注:常量池中存储的不一定是不变的量!如,private int x = 5,x是变量,但“x”这个变量名字依然存在常量池中。
② 常量池的特点
主要有如下特点:
常量池长度不固定
常量池中的常量由二维表来表示
常量池开头有个常量池容量计数器,接下来就是一个个 常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息。
常量池是class文件的资源仓库
常量池是与本class中其他部分关联最多的部分
常量池是class文件中空间占用最大的部分之一
在 JDK 的 bin 目录中,Oracle 公司已经为我们准备好一个专门用于分析 Class 文件字节码的工具:javap,这里使用javap -verbose ClassA.class
参数输出的 ClassA.class 文件字节码内容。
C:\Users\12746\Desktop>javap -verbose ClassA.class Classfile /C:/Users/12746/Desktop/ClassA.class Last modified 2018-9-2; size 675 bytes MD5 checksum c3f44107de7a63be7c3003d845c22f8e Compiled from "ClassA.java" public class com.test.classes.ClassA minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#25 // java/lang/Object."<init>":()V #2 = Fieldref #4.#26 // com/test/classes/ClassA.a:I #3 = Fieldref #4.#27 // com/test/classes/ClassA.b:I #4 = Class #28 // com/test/classes/ClassA #5 = Class #29 // java/lang/Object #6 = Utf8 a #7 = Utf8 I #8 = Utf8 b #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lcom/test/classes/ClassA; #16 = Utf8 getA #17 = Utf8 ()I #18 = Utf8 setA #19 = Utf8 (I)V #20 = Utf8 getB #21 = Utf8 setB #22 = Utf8 <clinit> #23 = Utf8 SourceFile #24 = Utf8 ClassA.java #25 = NameAndType #9:#10 // "<init>":()V #26 = NameAndType #6:#7 // a:I #27 = NameAndType #8:#7 // b:I #28 = Utf8 com/test/classes/ClassA #29 = Utf8 java/lang/Object { public com.test.classes.ClassA(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 10 7: putfield #2 // Field a:I 10: return LineNumberTable: line 6: 0 line 8: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/test/classes/ClassA; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/test/classes/ClassA; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 17: 0 line 18: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/test/classes/ClassA; 0 6 1 a I public static int getB(); descriptor: ()I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #3 // Field b:I 3: ireturn LineNumberTable: line 21: 0 public static void setB(int); descriptor: (I)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: putstatic #3 // Field b:I 4: return LineNumberTable: line 25: 0 line 26: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 b I static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: bipush 20 2: putstatic #3 // Field b:I 5: return LineNumberTable: line 10: 0 } SourceFile: "ClassA.java"
补充:Class类型的描述符
如果是数组的话,在元素的类型前面添加相应数量的 [ 符号,用 [ 的个数来提示数组的维度。
③ 常量池中常量的类型
常量池中的常量大体分为:字面值常量和符号引用。在此基础上,根据常量的数据类型不同,又可以被细分为14种常量类型。这14种常量类型都有各自的二维表示结构。每种常量类型的头1个字节都是tag,用于表示当前常量属于14种类型的哪一种。
我们也可以把常量池当做一个数组(常量池中的每一项常量都是一个表),与一般数组不同的是,常量池数组中不同的元素类型,结构都是不同的,长度当然也不相同;但是每一个元素的第一个数据都是u1类型,该字节是个标志位,占一个字节。JVM在解析长量池时,会根据这个u1类型来获取元素的具体类型。
目前,常量池中出现的常量类型有14种,如下表:
类型 | 标志 | 描述 |
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
有了这张表就可以继续剖析常量池的内容了,常量池第一个字节就是一个标志位,0x000A=10,说明第一个常量类型是CONSTANT_Methodref_info。这是一个表类型,它对应的结构是:
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
可知,该类型常量占1+2+2=5个字节。所以我们从常量池前5个字节就是第一个常量元素了。紧接后面就是第二个常量,同样的,开始是一个标志位,即0x009=9。可知,第二个常量是CONSTANT_Fieldref_info类型,CONSTANT_Fieldref_info用于表示字段的符号引用。如此可对照找出29个常量。
第一个常量是CONSTANT_Methodref_info类型的,它描述了类中方法的符号引用。class_index 项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口。
class_index表示的索引值是0x0005=5。根据之前javap -verbose输出的常量池信息,我们可以知道常量池的#5项是CONSTANT_Class_info类型的常量。该类型常量用于表示类或接口,格式如下。
CONSTANT_Class_info
表
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
tag表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表示一个类或接口的全限定名)。
name_index 项的值,必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,代表一个有效的类或接口二进制名称的内部形式。
name_index表示这个类或接口全限定名的位置,它的值表示指向常量池中的第几个常量。
name_index 表示的索引值是29(这里我直接从上面的量池信息读出,如果从字节码里看,此处的值为0x001D=29)
它会指向一个CONSTANT_Utf8_info
类型的常量,该类型的常量二维表结构如下:
CONSTANT_Utf8_info
表
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
CONSTANT_Utf8_info表示字符串常量;
tag表示当前常量的类型,这里应该是1;
length 项的值指明了 bytes[]数组的长度,bytes[]是表示字符串值的byte数组;
bytes为这个字符串的内容(采用缩略的UTF8编码)。
第一个字节是标志位,0x0001=1。说明此常量类型是CONSTANT_Utf8_info。后面2个字节是0x0010=16,表示后面bytes[]长度为16。所以往后数1个字节就是整个它表示的字符串常量。
bytes[]第一个字节值,0x006A。根据 ASCII码对照表,代表的字符串是"j"。依次的,第二个字节0x0061,代表“a”,等等。把16个字节看完你就得到了字符串常量表示“java/lang/Object”。好了这表示一个类的全限定名。饶了一大圈,终于找到最终要表示的常量信息了。
至此,我们把第一个常量的结构中的class_index就解析完了,还剩一个name_and_type_index。它表示了常量池在该索引处的项必须是 CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。
④ UTF-8编码与缩略UTF-8编码
前者每个字符使用3个字节表示,而后者把128个ASCII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。
⑤ 为什么Java中定义的类、变量的名字必须小于64K?
类、接口、变量等名字都属于符号引用,它们都存储在常量池中。而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info
类型的常量表示,这种类型的常量使用u2存储字符串的长度。由于2字节(16位)最多能表示65535个数,因此这些名字的最大长度最多只能是64K。
⑥ Class文件的常量池与运行时常量池区别
前面JVM的内存模型中讲过方法区中有一个运行时常量池。那么此处Class文件中的常量池与运行时常量池什么区别?
CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。也就是说运行时常量池中的常量,基本来源于各个class文件中的常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm一般不会自动添加常量到常量池。
【6】Class文件的构成4–访问标志
在常量池之后是2字节的访问标志–access_flags
。访问标志是用来表示这个class文件是类还是接口、是否被public、abstract、final修饰。
由于这些标志都由是/否表示,因次可以用0/1表示。
访问表示为2字节,可以表示16为标志,但JVM目前只定义了8种,未定义的直接写0。
具体访问标志如下表所示:
标志名 | 标志值 | 标志含义 | 针对的对像 |
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
ACC_SUPER
标志含义为使用新的invokespecial语义 。 invokespecial是一个字节码指令, 用于调用一个方法, 一般情况下, 调用构造方法或者使用super关键字显示调用父类的方法时, 会使用这条字节码指令。 这正是ACC_SUPER这个名字的由来。
在java 1.2之前, invokespecial对方法的调用都是静态绑定的, 而ACC_SUPER这个标志位在java 1.2的时候加入到class文件中, 它为invokespecial这条指令增加了动态绑定的功能。
还有一点需要说明, 既然access_flags出现在class文件中的类的层面上, 那么它只能描述类型的修饰符, 而不能描述字段或方法的修饰符。
【7】Class文件的构成5–类索引、父类索引和接口索引集合
类索引、父类索引和接口索引集合是用来表示当前class文件所表示类的名字、父类名字以及接口们的名字。
它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名。
由于一个类实现的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后。这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引。
【8】Class文件的构成6–字段表的集合
① 什么是字段表集合
字段表集合用于存储本类所涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。
每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。
② 字段表结构定义
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
access_flags
字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。
name_index
本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。
descriptor_index
描述符。用于描述本字段在Java中的数据类型等信息。
attributes_count
属性表集合的长度。
attributes
属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值(类成员变量初始值)。
③ 描述符
成员变量(包括静态成员变量和实例成员变量)和方法都有各自的描述符。
对于字段而言,描述符用于描述字段的数据类型。
对于方法而言,描述符用于描述字段的数据类型、参数类别和返回值。
在描述符中,基本数据类型用大写字母表示,对象类型用L对象类型的全限定名表示,数据用[数组类型的全限定名表示。
描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且参数之间无需任何符号。
④ 字段表集合注意点
一个class文件的字段表集合不能出现从父类/接口继承而来的字段。
一个class文件的字段表集合可能会出现程序中没有定义的字段
如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。
Java中只要一个类中两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段(联想方法重载)。
【9】Class文件的构成7–方法表的集合
在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。
方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。
方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。
方法表集合的注意点
如果本class没有重写父类的方法,那么本class文件的方法表集合是不会出现父类/父接口的方法表。
本class的方法表集合可能出现程序没有定义的方法
编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。
重载一个方法需要由相同的简单名称和不同的特征签名。
JVM的特征签名和Java的特征签名有所不同:
Java特征签名:方法参数在常量池中的字段符号引用的集合;
JVM特征签名:方法参数+返回值。