我们知道javac 命令可以将 .java 文件编译成 .class 文件,而这个Class 文件 中包含了Java虚拟机指令集、符号表以及若干其他辅助信息;最终将在Java虚拟机运行。
- Java虚拟机最终运行的是
Class 文件,它其实不管你是不是由.java文件生成的,如果有别的程序语言能将自己程序逻辑翻译成一个Class 文件,那么它也可以运行在 Java虚拟机 上的。- 也就是说Java虚拟机其实并不和
java语言强绑定,它只与Class这个二进制文件强关联,Class 文件包含程序运行的所有信息。
本文是以 JVM8 为例的。
一. class 文件格式
每一个 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];
}
- 每一个
Class文件都对应着唯一一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(比如类或接口也可以通过类加载器直接生成)。Class文件并不一定是一个文件,它指的是是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。多字节数据项总是按照big-endian(大端在前)的顺序存储。Class文件有两种数据格式:
- 一种是固定字节数的数据结构,称之为项(
Items),其实就是无符号数。例如上图u2表示两个字节无符号数(范围0 -> 65536)。- 一种是可变长度字节数的数据结构,称之为表(
Tables),用来记录复合数据结构,都是以_info结尾。例如上图中的cp_info,field_info,method_info和attribute_info。
先简单介绍一下 ClassFile 文件结构各部分含义:
-
magic:Class文件的魔数,代表这个二进制文件是Class文件,它的固定是0xCAFEBABE,不会改变。 -
minor_version和major_version: 表示这个Class文件的副版本号和主版本号。 -
constant_pool_count: 表示常量池的数量。它的类型是
u2,也就是说一个Class文件最多有65536 -1个常量。 -
constant_pool[constant_pool_count-1]: 表示常量池,记录这个Class文件所有常量信息。- 常量池包含
Class文件及其子结构中所有的常量信息。 - 常量池中的每一项都有一个特征,第一个字节表示这个常量的类型,在
JVM8中一共有14中类型。 - 常量池的索引范围是
1 -> constant_pool_count -1,也就是constant_pool_count -1个常量,索引0表示不是任何常量。
- 常量池包含
-
access_flags: 表示这个Class文件访问标志。- 我们知道在
java语言中,类,方法,成员字段都可以设置不同的修饰符,这里的访问标志就表示修饰符相关信息。 - 它的类型是
u2,有16位二进制数,也就是说它最多可以表示16个不同的访问标志。
- 我们知道在
-
this_class: 表示当前类或者接口的类常量索引。它的类型是
u2,就是表示常量池中某项的有效索引值,而且它指向的那个常量必须是CONSTANT_Class_info类型。 -
super_class: 表示当前类或者接口的父类常量索引。- 它的类型也是
u2,也表示常量池中某项的有效索引值。但是根据当前Class文件是类或者接口,分为两种情况:- 如果当前是类的话,当
super_class值是0,那就表示这个类是Object类;其他的情况,super_class值都是常量池中一个CONSTANT_Class_info类型常量的索引值。 - 如果当前是接口的话,那么
super_class值只会是常量池中一个CONSTANT_Class_info类型常量的索引值,而且这个CONSTANT_Class_info类型表示Object类,因为接口的父类就只能是Object。
- 如果当前是类的话,当
- 它的类型也是
-
interfaces_count: 表示当前类实现的接口数量,或者当前接口继承的接口数量。它的类型是
u2,表示最多有65536个接口;如果是0表示没有任何接口。 -
interfaces[interfaces_count]: 表示接口对应的常量池索引值表,长度就是interfaces_count。 -
fields_count: 表示当前类或者接口的字段数量,包括静态字段和成员字段。 -
fields[fields_count]: 表示当前类或者接口的字段详细信息的表,长度是fields_count。它的类型是
field_info,记录着这个字段的所有信息。 -
methods_count: 表示当前类或者接口的方法数量,包括类方法和实例方法。 -
methods[methods_count]: 表示当前类或者接口的方法详细信息的表,长度是fields_count。它的类型是
method_info,记录着这个方法的所有信息。 -
attributes_count: 表示当前类或者接口的属性数量。 -
attributes[attributes_count]: 表示当前类或者接口的属性详细信息的表。它的类型是
attribute_info,属性的种类非常多,不仅在类中有,在字段详情field_info和方法详情method_info中也有相关属性表。这个我们后面会慢慢说明。
二. 各类名称在 Class 文件 中的表示形式
- 全限定名
类和接口的名称都是全限定形式,被称为二进制名称,不过和
java语言中不同,它使用(/) 而不是 (.) 作为分隔符的,例如java.lang.Object就变成了java/lang/Object。 - 非限定名
- 方法名,字段名,局部变量名和形式参数名都是非限定形式。
- 非限定名至少有一个
Unicode字符,但是不能是ASCII字符 (. ; [ /) 这四个字符。 - 除了实例初始化方法
<init>和类初始化方法<clinit>以外,其他非限定名也不能出现 (< >) 这两个字符。
三. 描述符
描述符是表示字段或方法类型的字符串。
3.1 字段描述符
字段描述符表示类、实例或局部变量的类型。
字段描述符语法:
1. FieldDescriptor -> FieldType
2. FieldType -> BaseType | ObjectType | ArrayType
3. BaseType -> B | C | D | F | I | J | S | Z
4. ObjectType -> LClassName;
5. ArrayType -> [ComponentType
6. ComponentType -> FieldType
这个语法中,
B C D F I J S Z L ; [是终结符,其他的都是非终结符。这个方面不清楚的请看我的 编译原理-文法定义 相关文章说明。
从上面文法可以看出,字段描述符中一共有三个类型:
-
BaseType表示基础类型。- 一共分为八种,分别是
B C D F I J S Z。 - 其中
Z表示boolean类型,因为B已经表示byte类型了;J表示long类型,因为L这个字符被引用类型用了。
- 一共分为八种,分别是
-
ObjectType表示引用类型。它的格式是
LClassName;,即以L开头,;结尾,中间是类的全限定名。例如Object类就是Ljava/lang/Object;。 -
ArrayType表示数组类型。它的格式是
[ComponentType,即以[开头,ComponentType表示三个类型中任意类型。例如int[]就是[I;int[][]就是[[I;String[]就是[Ljava/lang/String;。
3.2 方法描述符
方法描述符包含0个或者多个参数描述符以及一个返回值描述符。
方法描述符语法:
1. MethodDescriptor -> ({ParameterDescriptor}) ReturnDescriptor
2. ParameterDescriptor-> FieldType
3. ReturnDescriptor-> FieldType | VoidDescriptor
4. VoidDescriptor-> V
- 这个语法中
( ) V这个三个字符是终结符。{ParameterDescriptor}中的{ }表示这个非终结符ParameterDescriptor能够出现0次或者多次。V表示没有任何返回值,就是void关键字。- 例如方法
String test(int i, long l, Integer i1, Long l1, Object[] objs) {..}对应的描述符(IJLjava/lang/Integer;Ljava/lang/Long;[Ljava/lang/Object;)Ljava/lang/String;
看了描述符,可能大家有点疑惑,泛型信息怎么表示啊?
描述符的确不能记录泛型相关信息,泛型信息记录在
Signatures属性中。
四. 常量池
常量池的通用格式如下:
cp_info {
u1 tag;
u1 info[];
}
一个字节无符号数
tag表示常量类型;info表示不同常量类型的数据。
目前 JVM8 中一共用14 种常量类型,分别如下:
| Constant Type | Value | 描述 |
|---|---|---|
| CONSTANT_Utf8 | 1 | 表示 Utf8 编码的字符串 |
| CONSTANT_Integer | 3 | 表示整形字面量 |
| CONSTANT_Float | 4 | 表示单精度浮点型字面量 |
| CONSTANT_Long | 5 | 表示长整形字面量 |
| CONSTANT_Double | 6 | 表示双精度浮点型字面量 |
| CONSTANT_Class | 7 | 表示类或者接口的符号引用 |
| CONSTANT_String | 8 | 表示字符串类型字面量 |
| CONSTANT_Fieldref | 9 | 表示字段的符号引用 |
| CONSTANT_Methodref | 10 | 表示类中方法的符号引用 |
| CONSTANT_InterfaceMethodref | 11 | 表示接口中方法的符号引用 |
| CONSTANT_NameAndType | 12 | 表示字段或者方法的名称和描述符 |
| CONSTANT_MethodHandle | 15 | 表示方法的句柄 |
| CONSTANT_MethodType | 16 | 表示方法的类型 |
| CONSTANT_InvokeDynamic | 18 | 表示动态计算常量 |
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
CONSTANT_MethodHandle_info {
u1 tag;
u1 reference_kind;
u2 reference_index;
}
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
4.1 CONSTANT_Utf8_info
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
-
tag: 表示常量类型,值就是1(CONSTANT_Utf8) -
length: 表示Utf8字符串的总字节数。它的类型是
u2,也就是Utf8字符串最多只能有65536个字节。 -
bytes[length]: 表示Utf8字符串的字节数组数据。
4.2 CONSTANT_Integer_info 和 CONSTANT_Float_info
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
-
tag: 表示常量类型。-
CONSTANT_Integer_info的值就是3(CONSTANT_Integer)。 -
CONSTANT_Float_info的值就是4(CONSTANT_Float)。
-
-
bytes: 是一个四个字节数,用来储存int和float类型字面量。
4.3 CONSTANT_Long_info 和 CONSTANT_Double_info
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
-
tag: 表示常量类型。-
CONSTANT_Long_info的值就是5(CONSTANT_Long)。 -
CONSTANT_Double_info的值就是6(CONSTANT_Double)。
-
-
high_bytes和low_bytes: 组成一个8个字节数,来储存long和double类型字面量。 - 特别注意,这两种类型会在常量表中占据两个索引,也就是如果索引
3处的CONSTANT_Long_info类型,那么下一个有效索引是5。
4.4 CONSTANT_Class_info
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
-
tag: 表示常量类型,CONSTANT_Class_info的值就是7(CONSTANT_Class)。 -
name_index: 表示类符号引用的索引。值必须是当前常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。
4.5 CONSTANT_String_info
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
-
tag: 表示常量类型,CONSTANT_String_info的值就是8(CONSTANT_String)。 -
string_index: 表示字符串值的索引。值必须是当前常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。
4.6 CONSTANT_Fieldref_info , CONSTANT_Methodref_info 和 CONSTANT_InterfaceMethodref_info
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
-
tag: 表示常量类型。-
CONSTANT_Fieldref_info的值就是9(CONSTANT_Fieldref); -
CONSTANT_Methodref_info的值就是10(CONSTANT_Methodref); -
CONSTANT_InterfaceMethodref_info的值就是11(CONSTANT_InterfaceMethodref)。
-
-
class_index: 表示这个字段或者方法所属类符号引用的索引。- 值必须是当前常量池中一个
CONSTANT_Class_info类型常量的有效索引。
- 值必须是当前常量池中一个
-
name_and_type_index: 表示这个字段或者方法的名称和描述符的索引。- 值必须是当前常量池中一个
CONSTANT_NameAndType_info类型常量的有效索引。 - 知道字段的名称和描述符,就知道了字段的名字和类型。
- 知道方法的名称和描述符,就知道了方法的名字和方法参数类型以及方法返回值类型。
- 值必须是当前常量池中一个
我们知道要使用一个字段或者调用一个方法,就必须知道字段或者方法所属类符号引用,和字段的名字和类型,方法的名字和方法参数类型以及方法返回值类型。
但是我们知道类是能继承的,那么子类调用父类的方法或者字段,这里的所属类符号引用,到底是子类本身还是父类的呢?
请大家思考一下,后面的例子中,我们将会讲解。
4.7 CONSTANT_NameAndType_info
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
-
tag: 表示常量类型,CONSTANT_NameAndType_info的值就是12(CONSTANT_NameAndType)。 -
name_index: 表示字段或者方法名称。值必须是当前常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。 -
descriptor_index: 表示字段或者方法描述符。值必须是当前常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。
4.8 CONSTANT_MethodHandle_info
CONSTANT_MethodHandle_info {
u1 tag;
u1 reference_kind;
u2 reference_index;
}
-
tag: 表示常量类型,CONSTANT_MethodHandle_info的值就是15(CONSTANT_MethodHandle)。 -
reference_kind:表示方法句柄的类型。值范围是
0 ~ 9,不同的值决定了句柄字节码的行为。 -
reference_index:就是常量池中的一个有效索引,因为reference_kind值的不同,分为几种情况:- 如果
reference_kind的值是1(REF_getField),2
(REF_getStatic),3(REF_putField), 或者4(REF_putStatic);都是和字段相关,那么reference_index必须是常量池中CONSTANT_Fieldref_info类型常量的有效索引。 - 如果
reference_kind的值是5(REF_invokeVirtual) 或者8
(REF_newInvokeSpecial),那么reference_index必须是常量池中CONSTANT_Methodref_info类型常量的有效索引,表示一个类中的实例方法或者构造函数。 - 如果
reference_kind的值是6(REF_invokeStatic)
或者7(REF_invokeSpecial),且class版本小于52(即JVM8),那么reference_index必须是CONSTANT_Methodref_info类型索引;如果 版本大于或者等于52,那么reference_index必须是CONSTANT_Methodref_info类型索引或者CONSTANT_InterfaceMethodref_info类型索引,因为JVM8以上接口有默认方法。 - 如果
reference_kind的值是9(REF_invokeInterface),那么reference_index必须是CONSTANT_InterfaceMethodref_info类型索引。 - 如果
reference_kind的值是5(REF_invokeVirtual),6
(REF_invokeStatic),7(REF_invokeSpecial), or9(REF_invokeInterface),那么reference_index必须是CONSTANT_Methodref_info类型索引或者CONSTANT_InterfaceMethodref_info类型索引,但是不能是实例初始化方法<init>和 类初始化方法<clinit>。 - 如果
reference_kind的值是8(REF_newInvokeSpecial),那么reference_index必须是CONSTANT_Methodref_info类型索引,而且它就是实例初始化方法<init>。
- 如果
4.9 CONSTANT_MethodType_info
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
-
tag: 表示常量类型,CONSTANT_MethodType_info的值就是16(CONSTANT_MethodType)。 -
descriptor_index: 表示方法的描述符。值必须是当前常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。
4.10 CONSTANT_InvokeDynamic_info
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
-
tag: 表示常量类型,CONSTANT_InvokeDynamic_info的值就是18(CONSTANT_InvokeDynamic)。 -
bootstrap_method_attr_index: 表示引导方法bootstrap_methods中的有效索引。引导方法
bootstrap_methods记录在Class 文件的BootstrapMethods属性中。 -
name_and_type_index: 表示方法的名称和描述符的索引。值必须是当前常量池中一个
CONSTANT_NameAndType_info类型常量的有效索引。
五. 访问标志(access_flags)
我们知道类,方法,字段都有不同的访问标志,在Class 文件 中使用一个u2 类型数据项来存储,也就是最多可以有 16 个不同标志位。
在类,方法,字段中有相同的标志,也有不同的标志,总体规划,我们可以借助 Modifier 类的源码来了解:
public static final int PUBLIC = 0x00000001;
public static final int PRIVATE = 0x00000002;
public static final int PROTECTED = 0x00000004;
public static final int STATIC = 0x00000008;
public static final int FINAL = 0x00000010;
public static final int SYNCHRONIZED = 0x00000020;
public static final int VOLATILE = 0x00000040;
public static final int TRANSIENT = 0x00000080;
public static final int NATIVE = 0x00000100;
public static final int INTERFACE = 0x00000200;
public static final int ABSTRACT = 0x00000400;
public static final int STRICT = 0x00000800;
static final int BRIDGE = 0x00000040;
static final int VARARGS = 0x00000080;
static final int SYNTHETIC = 0x00001000;
static final int ANNOTATION = 0x00002000;
static final int ENUM = 0x00004000;
static final int MANDATED = 0x00008000;
- 因为
access_flags是两个字节数,这里使用int类型,也就说前面4个永远是0(0x0000----)。- 这里
0x00000040和0x00000080重复使用了,但是没关系,因为表示不同的访问标志。
5.1 类的访问标志
在 Modifier 类中,类的访问标志:
private static final int CLASS_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
Modifier.ABSTRACT | Modifier.STATIC | Modifier.FINAL |
Modifier.STRICT;
private static final int INTERFACE_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
Modifier.ABSTRACT | Modifier.STATIC | Modifier.STRICT;
我们知道在 java 中类可以用的修饰符有: public,protected,private,abstract,static,final,strictfp。
- 其中
protected,private和static都是只能用在内部类里面。strictfp这个关键字表示这个类精确进行浮点运算。不过这么多年也没有看谁用过。final关键字不能用在接口上。
但是我们再看 Class 文件 中类的访问标志:
| 标志名 | 值 | 描述 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 声明为 public,可以从包外访问 |
| ACC_FINAL | 0x0010 | 声明为 final,不允许被继承 |
| ACC_SUPER | 0x0020 | 为了兼容之前编译器编译代码而设置的,目前编译器编译的代码,这个标志位都是1。 |
| ACC_INTERFACE | 0x0200 | 表示是类还是接口 |
| ACC_ABSTRACT | 0x0400 | 声明为 abstract,不能被实例化 |
| ACC_SYNTHETIC | 0x1000 | 声明为 synthetic,表示这个 Class 文件 不在源代码中 |
| ACC_ANNOTATION | 0x2000 | 表示为注解类型 |
| ACC_ENUM | 0x4000 | 表示为枚举类型 |
仔细看,你会发现有些不同点:
- 内部类中的三个修饰符信息,没有出现在
Class 文件中类的访问标志里。的确是这样,三个修饰符只会控制
javac编译行为的不同,编译完的内部类class文件中,是没有这三个修饰符信息。例如,非static修饰的内部类,javac编译的时候,会在class文件中,添加一个字段,类型就是外部类。 -
ACC_INTERFACE,ACC_ANNOTATION和ACC_ENUM分别表示java目前的三种类型interface,@interface和enum。
5.2 字段的访问标志
在 Modifier 类中,字段的访问标志:
private static final int FIELD_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
Modifier.STATIC | Modifier.FINAL | Modifier.TRANSIENT |
Modifier.VOLATILE;
我们知道在 java 中字段可以用的修饰符有: public,protected,private,static,final,transient 和 volatile。
- 其中
transient表示这个字段是瞬时,进行java序列化的时候,不会序列化被transient修饰的字段。volatile表示这个字段是可见的。volatile关键字的详细介绍请看 Java多线程详细介绍。
但是我们再看 Class 文件 中字段的访问标志:
| 标志名 | 值 | 描述 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 声明为 public,可以从包外访问 |
| ACC_PRIVATE | 0x0002 | 声明为 private,只能在定义该字段的类中访问 |
| ACC_PROTECTED | 0x0004 | 声明为 protected,子类可以访问 |
| ACC_STATIC | 0x0008 | 声明为 static
|
| ACC_FINAL | 0x0010 | 声明为 final,表示对象构造完成后,不能直接修改该字段了 |
| ACC_VOLATILE | 0x0040 | 声明为 volatile
|
| ACC_TRANSIENT | 0x0080 | 声明为 transient
|
| ACC_SYNTHETIC | 0x1000 | 表示该字段不是在源码中,由编译器生成 |
| ACC_ENUM | 0x4000 | 表示该字段是枚举(enum)类型 |
Class 文件 中字段的访问标志和java 中字段的修饰符差不多,只是多了 ACC_SYNTHETIC 和 ACC_ENUM 两个标志。
5.3 方法的访问标志
在 Modifier 类中,方法的访问标志:
private static final int CONSTRUCTOR_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE;
private static final int METHOD_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
Modifier.ABSTRACT | Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE | Modifier.STRICT;
我们知道在 java 中方法可以用的修饰符有:
public,protected,private,abstract,static,final,synchronized, synchronized和 strictfp。
但是我们再看 Class 文件 中方法的访问标志:
| 标志名 | 值 | 描述 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 声明为 public,可以从包外访问 |
| ACC_PRIVATE | 0x0002 | 声明为 private,只能在定义该字段的类中访问 |
| ACC_PROTECTED | 0x0004 | 声明为 protected,子类可以访问 |
| ACC_STATIC | 0x0008 | 声明为 static
|
| ACC_FINAL | 0x0010 | 声明为 final, 表示方法不能被覆盖 |
| ACC_SYNCHRONIZED | 0x0020 | 声明为 synchronized, 表示对方法的调用,会包装在同步锁里 |
| ACC_BRIDGE | 0x0040 | 声明为 bridge,由编译器产生 |
| ACC_VARARGS | 0x0080 | 表示方法带有变长参数 |
| ACC_NATIVE | 0x0100 | 声明为 native, 不是由 java 语言实现 |
| ACC_ABSTRACT | 0x0400 | 声明为 abstract ,该方法没有实现方法 |
| ACC_STRICT | 0x0800 | 声明为 strictfp,使用精确浮点模式 |
| ACC_SYNTHETIC | 0x1000 | 该方法由编译器合成的,不是由源码编译出来的 |
六. 字段和方法
6.1 字段
字段详情 field_info 的格式如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
-
access_flags: 表示字段访问标志,在上面已经介绍了。 -
name_index: 表示字段的名称。值必须是常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。 -
descriptor_index: 表示字段的描述符。值必须是常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。 -
attributes_count: 表示当前字段的属性数量。 -
attributes[attributes_count]: 表示当前字段的属性详细信息的表。
6.2 方法
方法详情 method_info 的格式如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
-
access_flags: 表示方法访问标志,在上面已经介绍了。 -
name_index: 表示方法的名称。值必须是常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。 -
descriptor_index: 表示方法的描述符。值必须是常量池中一个
CONSTANT_Utf8_info类型常量的有效索引。 -
attributes_count: 表示当前方法的属性数量。 -
attributes[attributes_count]: 表示当前方法的属性详细信息的表。
关于 Class 文件 中属性相关信息,我们再后面章节介绍。
七. 例子
我们可以通过 javap 的命令来阅读 Class 文件 中相关信息。
7.1 最简单的例子
package com.zhang.jvm.reflect.example;
public class T {
}
这个是最简单的一个类,没有任何字段和方法,只继承Object 类,我们来看看它编译后的字节码信息,通过javap -p -v T.class 的命令:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 288 bytes
MD5 checksum 2771c8258a6734d72812fca914966e07
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // com/zhang/jvm/reflect/example/T
#3 = Class #15 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#11 = Utf8 SourceFile
#12 = Utf8 T.java
#13 = NameAndType #4:#5 // "<init>":()V
#14 = Utf8 com/zhang/jvm/reflect/example/T
#15 = Utf8 java/lang/Object
{
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
- 前面的
minor version: 0之前的都是.class文件本身的信息,这里魔数magic信息被省略了。- 然后依次就是常量池信息
Constant pool,字段相关信息(因为T.class没有字段,所以这里没有),方法相关信息({}里面的,就是T.class默认的构造器方法<init>),最后类属性相关信息(就是这里的SourceFile: "T.java")。this_class,super_class,interfaces相关信息就在public class com.zhang.jvm.reflect.example.T中,Object父类就隐藏。
我们重点关注常量池相关信息,会发现虽然T.class 很干净,但是也有15 个常量,来我们依次分析:
-
#1: 这是一个CONSTANT_Methodref_info类型,值是java/lang/Object."<init>":()V。很明显它就是
Object类构造器方法的符号引用,通过这个引用来调用Object类构造器方法。 -
#2: 这是一个CONSTANT_Class_info类型,值是com/zhang/jvm/reflect/example/T。就是本类的全限定名称。
-
#3: 这是一个CONSTANT_Class_info类型,值是java/lang/Object。 -
#4: 这是一个CONSTANT_Utf8_info类型,值是<init>。默认构造器方法的名称。
-
#5: 这是一个CONSTANT_Utf8_info类型,值是()V。默认构造器方法的描述符。
-
#6: 这是一个CONSTANT_Utf8_info类型,值是Code,是一个属性名称。 -
#7: 这是一个CONSTANT_Utf8_info类型,值是LineNumberTable,是一个属性名称。 -
#8: 这是一个CONSTANT_Utf8_info类型,值是LocalVariableTable,是一个属性名称。 -
#9: 这是一个CONSTANT_Utf8_info类型,值是this。 -
#10: 这是一个CONSTANT_Utf8_info类型,值是Lcom/zhang/jvm/reflect/example/T;。它是
T类型的描述符。 -
#11: 这是一个CONSTANT_Utf8_info类型,值是SourceFile,是一个属性名称。 -
#12: 这是一个CONSTANT_Utf8_info类型,值是T.java。 -
#13: 这是一个CONSTANT_Utf8_info类型,值是"<init>":()V。它是构造器方法的描述符。
-
#14: 这是一个CONSTANT_Utf8_info类型,值是com/zhang/jvm/reflect/example/T。就是本类的全限定名称。
-
#14: 这是一个CONSTANT_Utf8_info类型,值是java/lang/Object。
7.2 有字段和方法的例子
package com.zhang.jvm.reflect.example;
public class T {
private String name;
public void test() {}
}
与之前的例子相比较,多了一个字段和方法,那么得到的字节码信息如下:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 388 bytes
MD5 checksum f97a6c1995036e9605c0121916c3d815
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // com/zhang/jvm/reflect/example/T
#3 = Class #18 // java/lang/Object
#4 = Utf8 name
#5 = Utf8 Ljava/lang/String;
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#13 = Utf8 test
#14 = Utf8 SourceFile
#15 = Utf8 T.java
#16 = NameAndType #6:#7 // "<init>":()V
#17 = Utf8 com/zhang/jvm/reflect/example/T
#18 = Utf8 java/lang/Object
{
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhang/jvm/reflect/example/T;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
与之前的相比较,发现有三个变化:
- 常量池中多了三个常量,都是
CONSTANT_Utf8_info类型的,分别是name,Ljava/lang/String;和test。- 多了一个字段
name相关信息。- 多了一个方法
test相关信息。
但是你会发现常量池中怎么没有这个字段name 的CONSTANT_Fieldref_info 类型的常量呢?
那是因为我们没有使用这个字段。
package com.zhang.jvm.reflect.example;
public class T {
private String name;
public void test() {}
public void test1() {
name = "12";
test();
}
}
多写了一个方法test1 来调用name 字段和 test 方法,那么得到的字节码信息如下:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 499 bytes
MD5 checksum 2ddae70db9ea9f755f9312eb2b2f2d07
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = String #21 // 12
#3 = Fieldref #5.#22 // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
#4 = Methodref #5.#23 // com/zhang/jvm/reflect/example/T.test:()V
#5 = Class #24 // com/zhang/jvm/reflect/example/T
#6 = Class #25 // java/lang/Object
#7 = Utf8 name
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#16 = Utf8 test
#17 = Utf8 test1
#18 = Utf8 SourceFile
#19 = Utf8 T.java
#20 = NameAndType #9:#10 // "<init>":()V
#21 = Utf8 12
#22 = NameAndType #7:#8 // name:Ljava/lang/String;
#23 = NameAndType #16:#10 // test:()V
#24 = Utf8 com/zhang/jvm/reflect/example/T
#25 = Utf8 java/lang/Object
{
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhang/jvm/reflect/example/T;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/zhang/jvm/reflect/example/T;
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #2 // String 12
3: putfield #3 // Field name:Ljava/lang/String;
6: aload_0
7: invokevirtual #4 // Method test:()V
10: return
LineNumberTable:
line 10: 0
line 11: 6
line 12: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
与之前的相比较,发现如下变化:
- 常量池中多了七个常量,分别是字符串
"12"对应的两个常量#2和#21; 字段name对应的两个常量#3和#22;方法test对应的两个常量#4和#23; 以及方法test1的名称常量#17。- 多了一个方法
test1相关信息。
7.3 继承的例子
package com.zhang.jvm.reflect.example;
public class TParent {
public String name;
public void say(){}
}
package com.zhang.jvm.reflect.example;
public class T extends TParent {
public void test() {
name = "T";
say();
}
}
这里定义一个父类TParent,有一个公共字段name和方法say。子类T 继承TParent类,并有一个方法test 调用父类的字段和方法,来看T 的字节码信息:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 452 bytes
MD5 checksum aeea52a9b2b166d588e1336dd0a4dcc1
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // com/zhang/jvm/reflect/example/TParent."<init>":()V
#2 = String #18 // T
#3 = Fieldref #5.#19 // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
#4 = Methodref #5.#20 // com/zhang/jvm/reflect/example/T.say:()V
#5 = Class #21 // com/zhang/jvm/reflect/example/T
#6 = Class #22 // com/zhang/jvm/reflect/example/TParent
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#14 = Utf8 test
#15 = Utf8 SourceFile
#16 = Utf8 T.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Utf8 T
#19 = NameAndType #23:#24 // name:Ljava/lang/String;
#20 = NameAndType #25:#8 // say:()V
#21 = Utf8 com/zhang/jvm/reflect/example/T
#22 = Utf8 com/zhang/jvm/reflect/example/TParent
#23 = Utf8 name
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 say
{
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhang/jvm/reflect/example/T;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #2 // String T
3: putfield #3 // Field name:Ljava/lang/String;
6: aload_0
7: invokevirtual #4 // Method say:()V
10: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
- 你会发现字段
name在常量池中#3(Fieldref)和方法say在常量池中#4(Methodref),它们所属类都是T,而不是TParent。- 但是你又发现在
T的字节码文件中,就没有name字段相关信息和say方法相关信息。- 这个是没有关系的,因为只要父类中相关字段和方法访问权限是可以的,那么子类找不到也会到父类去找的。
但是如果你就想调用父类的该怎么办呢?
这个很好办,我们知道
java中有super关键字。
package com.zhang.jvm.reflect.example;
public class T extends TParent {
public void test() {
super.name = "T";
super.say();
}
}
再来看T的字节码信息:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 452 bytes
MD5 checksum 7d0901b392b0bfd74300cb87482ba183
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // com/zhang/jvm/reflect/example/TParent."<init>":()V
#2 = String #18 // T
#3 = Fieldref #6.#19 // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
#4 = Methodref #6.#20 // com/zhang/jvm/reflect/example/TParent.say:()V
#5 = Class #21 // com/zhang/jvm/reflect/example/T
#6 = Class #22 // com/zhang/jvm/reflect/example/TParent
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#14 = Utf8 test
#15 = Utf8 SourceFile
#16 = Utf8 T.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Utf8 T
#19 = NameAndType #23:#24 // name:Ljava/lang/String;
#20 = NameAndType #25:#8 // say:()V
#21 = Utf8 com/zhang/jvm/reflect/example/T
#22 = Utf8 com/zhang/jvm/reflect/example/TParent
#23 = Utf8 name
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 say
{
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhang/jvm/reflect/example/T;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #2 // String T
3: putfield #3 // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
6: aload_0
7: invokespecial #4 // Method com/zhang/jvm/reflect/example/TParent.say:()V
10: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
- 我们发现字段
name在常量池中#3(Fieldref)和方法say在常量池中#4(Methodref),它们所属类都变成了TParent。- 还有一点需要特别注意的,就是在
test方法的指令集中,调用say方法的指令,从invokevirtual指令变成invokespecial指令。
子类可以直接使用父类允许访问权限的字段和方法,即使子类中没有相关字段和方法,这个是继承的功效。
但是我们知道面向对象语言,除了继承的特性,还有一个多态特性。
- 多态就是根据运行时,对象实际类型来调用对应方法,而不是编译时写死的类型去调用,所谓的写死类型就是常量池中
Methodref类型常量中所属的类型。- 我们知道方法是具有多态特性的,那么字段也有多态么。
package com.zhang.jvm.reflect.example;
public class TParent {
public String name = "TParent";
public void say(){
System.out.println("I am TParent");
}
}
package com.zhang.jvm.reflect.example;
public class T extends TParent {
public String name = "T";
public void say(){
System.out.println("I am T");
}
public static void main(String[] agrs) {
TParent tParent = new T();
T t = (T) tParent;
System.out.println("tParent.name: "+ tParent.name+"\nt.name: "+t.name);
tParent.say();
t.say();
}
}
运行结果:
tParent.name: TParent
t.name: T
I am T
I am T
你会发现即使运行期是同一个对象,但是字段name 得到结果是不一样的,即字段是不能多态的;但是方法的确是按照运行期实际对象类型调用。下面看它的字节码信息:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
Last modified 2021-12-6; size 1070 bytes
MD5 checksum 3490fbda3bf98c7fbd556ef4c0f5f3f4
Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#38 // com/zhang/jvm/reflect/example/TParent."<init>":()V
#2 = String #39 // T
#3 = Fieldref #7.#40 // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
#4 = Fieldref #41.#42 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #43 // I am T
#6 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #46 // com/zhang/jvm/reflect/example/T
#8 = Methodref #7.#38 // com/zhang/jvm/reflect/example/T."<init>":()V
#9 = Class #47 // java/lang/StringBuilder
#10 = Methodref #9.#38 // java/lang/StringBuilder."<init>":()V
#11 = String #48 // tParent.name:
#12 = Methodref #9.#49 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#13 = Fieldref #18.#40 // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
#14 = String #50 // \nt.name:
#15 = Methodref #9.#51 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#16 = Methodref #18.#52 // com/zhang/jvm/reflect/example/TParent.say:()V
#17 = Methodref #7.#52 // com/zhang/jvm/reflect/example/T.say:()V
#18 = Class #53 // com/zhang/jvm/reflect/example/TParent
#19 = Utf8 name
#20 = Utf8 Ljava/lang/String;
#21 = Utf8 <init>
#22 = Utf8 ()V
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/zhang/jvm/reflect/example/T;
#28 = Utf8 say
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 agrs
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 tParent
#34 = Utf8 Lcom/zhang/jvm/reflect/example/TParent;
#35 = Utf8 t
#36 = Utf8 SourceFile
#37 = Utf8 T.java
#38 = NameAndType #21:#22 // "<init>":()V
#39 = Utf8 T
#40 = NameAndType #19:#20 // name:Ljava/lang/String;
#41 = Class #54 // java/lang/System
#42 = NameAndType #55:#56 // out:Ljava/io/PrintStream;
#43 = Utf8 I am T
#44 = Class #57 // java/io/PrintStream
#45 = NameAndType #58:#59 // println:(Ljava/lang/String;)V
#46 = Utf8 com/zhang/jvm/reflect/example/T
#47 = Utf8 java/lang/StringBuilder
#48 = Utf8 tParent.name:
#49 = NameAndType #60:#61 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#50 = Utf8 \nt.name:
#51 = NameAndType #62:#63 // toString:()Ljava/lang/String;
#52 = NameAndType #28:#22 // say:()V
#53 = Utf8 com/zhang/jvm/reflect/example/TParent
#54 = Utf8 java/lang/System
#55 = Utf8 out
#56 = Utf8 Ljava/io/PrintStream;
#57 = Utf8 java/io/PrintStream
#58 = Utf8 println
#59 = Utf8 (Ljava/lang/String;)V
#60 = Utf8 append
#61 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#62 = Utf8 toString
#63 = Utf8 ()Ljava/lang/String;
{
public java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public com.zhang.jvm.reflect.example.T();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
4: aload_0
5: ldc #2 // String T
7: putfield #3 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zhang/jvm/reflect/example/T;
public void say();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String I am T
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/zhang/jvm/reflect/example/T;
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: new #7 // class com/zhang/jvm/reflect/example/T
3: dup
4: invokespecial #8 // Method "<init>":()V
7: astore_1
8: aload_1
9: checkcast #7 // class com/zhang/jvm/reflect/example/T
12: astore_2
13: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #9 // class java/lang/StringBuilder
19: dup
20: invokespecial #10 // Method java/lang/StringBuilder."<init>":()V
23: ldc #11 // String tParent.name:
25: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: aload_1
29: getfield #13 // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
32: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: ldc #14 // String \nt.name:
37: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: aload_2
41: getfield #3 // Field name:Ljava/lang/String;
44: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
50: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
53: aload_1
54: invokevirtual #16 // Method com/zhang/jvm/reflect/example/TParent.say:()V
57: aload_2
58: invokevirtual #17 // Method say:()V
61: return
LineNumberTable:
line 12: 0
line 13: 8
line 15: 13
line 17: 53
line 18: 57
line 19: 61
LocalVariableTable:
Start Length Slot Name Signature
0 62 0 agrs [Ljava/lang/String;
8 54 1 tParent Lcom/zhang/jvm/reflect/example/TParent;
13 49 2 t Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
这个字节码信息比较多,我们主要观察 main 方法的指令集信息:
tParent.name对应第29行指令,使用的常量池索引是#13,是一个Fieldref类型,所属的类索引就是TParent。tParent.name对应第41行指令,使用的常量池索引是#3,是一个Fieldref类型,所属的类索引就是T。tParent.say()对应第54行指令,使用的常量池索引是#16,是一个Methodref类型,所属的类索引就是TParent。tParent.say()对应第58行指令,使用的常量池索引是#17,是一个Methodref类型,所属的类索引就是T。
主要的区别就是在于 invokevirtual 指令,就是它实现了方法多态的功能,它会根据运行期对象实际类型去匹配对应的方法,而不是根据这里 Methodref 常量中规定所属类去匹配。
7.4 内部类
在 java 语言中内部类其实是一个非常特殊的存在,里面有很多javac 编译器帮我们做的事情,如下是一个简单的内部类:
public class TS {
TInner inner = null;
public void test() {
inner.name = null;
}
class TInner {
private String name;
}
}
先看一下TS 的字节码:
Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/TS.class
Last modified 2021-12-6; size 637 bytes
MD5 checksum 5ebdb2d72b0b3d5a224c59860e8b386a
Compiled from "TS.java"
public class com.zhang.jvm.reflect.example.TS
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#22 // com/zhang/jvm/reflect/example/TS.inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
#3 = Methodref #6.#23 // com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
#4 = Class #24 // com/zhang/jvm/reflect/example/TS
#5 = Class #25 // java/lang/Object
#6 = Class #26 // com/zhang/jvm/reflect/example/TS$TInner
#7 = Utf8 TInner
#8 = Utf8 InnerClasses
#9 = Utf8 inner
#10 = Utf8 Lcom/zhang/jvm/reflect/example/TS$TInner;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/zhang/jvm/reflect/example/TS;
#18 = Utf8 test
#19 = Utf8 SourceFile
#20 = Utf8 TS.java
#21 = NameAndType #11:#12 // "<init>":()V
#22 = NameAndType #9:#10 // inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
#23 = NameAndType #27:#28 // access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
#24 = Utf8 com/zhang/jvm/reflect/example/TS
#25 = Utf8 java/lang/Object
#26 = Utf8 com/zhang/jvm/reflect/example/TS$TInner
#27 = Utf8 access$002
#28 = Utf8 (Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
{
com.zhang.jvm.reflect.example.TS$TInner inner;
descriptor: Lcom/zhang/jvm/reflect/example/TS$TInner;
flags:
public com.zhang.jvm.reflect.example.TS();
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: aconst_null
6: putfield #2 // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
9: return
LineNumberTable:
line 6: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/zhang/jvm/reflect/example/TS;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
4: aconst_null
5: invokestatic #3 // Method com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
8: pop
9: return
LineNumberTable:
line 11: 0
line 12: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/zhang/jvm/reflect/example/TS;
}
SourceFile: "TS.java"
InnerClasses:
#7= #6 of #4; //TInner=class com/zhang/jvm/reflect/example/TS$TInner of class com/zhang/jvm/reflect/example/TS
在这个类的字节码文件中,你会发现一个很有意思的事情,那就是在 test 方法指令集中,语句 inner.name = null 居然没有对应 putfield 指令,而是 invokestatic 指令,调用了TInner 中一个名为access$002 的类方法。
为什么会这样呢?
- 那是因为字段都是有访问权限的,而
TInner中name字段的访问权限是private,那么只有这个TInner类里面才可以访问它,也就是这个name字段只能在TInner类的常量池中生成对应的CONSTANT_field_info类型常量。- 但是
java语言规范里面,外部类又可以调用内部类的私有字段,所以javac编译器帮我们做了处理,在TInner类中生成了一个名为access$002静态方法来给私有字段赋值。
TInner 的字节码不能使用javap 命令看到,我就简单地说一下,有如下重点:
-
TInner字节码中有两个字段,name和this$0。this$0的类型就是Lcom/zhang/jvm/reflect/example/TS;,也就是说内部类都持有一个外部类类型的字段。 -
TInner字节码的构造器方法0 aload_0 1 aload_1 2 putfield #2 <com/zhang/jvm/reflect/example/TS$TInner.this$0> 5 aload_0 6 invokespecial #3 <java/lang/Object.<init>> 9 return也就是说内部类的构造函数肯定会将外部类的引用传递进来的,然后赋值给
this$0字段。 - 如果外部类给内部类的私有变量赋值了,那么就会生成一个静态方法,来进行内部类私有变量的赋值。