我们知道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
字符 (. ; [ /
) 这四个字符。 - 除了实例初始化方法
和类初始化方法
以外,其他非限定名也不能出现 (< >
) 这两个字符。
三. 描述符
描述符是表示字段或方法类型的字符串。
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
类型索引,但是不能是实例初始化方法
和 类初始化方法
。 - 如果
reference_kind
的值是8
(REF_newInvokeSpecial
),那么reference_index
必须是CONSTANT_Methodref_info
类型索引,而且它就是实例初始化方法
。
- 如果
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."":()V
#2 = Class #14 // com/zhang/jvm/reflect/example/T
#3 = Class #15 // java/lang/Object
#4 = Utf8
#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 // "":()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."":()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
默认的构造器方法),最后类属性相关信息(就是这里的
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."
。":()V 很明显它就是
Object
类构造器方法的符号引用,通过这个引用来调用Object
类构造器方法。 -
#2
: 这是一个CONSTANT_Class_info
类型,值是com/zhang/jvm/reflect/example/T
。就是本类的全限定名称。
-
#3
: 这是一个CONSTANT_Class_info
类型,值是java/lang/Object
。 -
#4
: 这是一个CONSTANT_Utf8_info
类型,值是
。默认构造器方法的名称。
-
#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
类型,值是"
。":()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."":()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
#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 // "":()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."":()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."":()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
#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 // "":()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."":()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."":()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
#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 // "":()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."":()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."":()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
#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 // "":()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."":()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."":()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."":()V
#9 = Class #47 // java/lang/StringBuilder
#10 = Methodref #9.#38 // java/lang/StringBuilder."":()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
#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 // "":()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."":()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 "":()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."":()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."":()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
#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 // "":()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."":()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
5 aload_0 6 invokespecial #3 > 9 return 也就是说内部类的构造函数肯定会将外部类的引用传递进来的,然后赋值给
this$0
字段。 - 如果外部类给内部类的私有变量赋值了,那么就会生成一个静态方法,来进行内部类私有变量的赋值。