类文件结构
类文件结构
一个编译后的类文件包含下面的结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_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];
}
magic, minor_version, major_version | 类文件的版本信息和用于编译这个类的 JDK 版本 |
---|---|
constant_pool | 类似于符号表,尽管它包含更多数据。下面有更多的详细描述, |
access_flags | 提供这个类的描述符列表, |
this_class | 提供这个类全名的常量池(constant_pool)索引,比如 org/jamesdbloom/foo/Bar, |
super_class | 提供这个类的父类符号引用的常量池索引, |
interfaces | 指向常量池的索引数组,提供那些被实现的接口的符号引用, |
fields | 提供每个字段完整描述的常量池索引数组, |
methods | 指向 constant_pool 的索引数组,用于表示每个方法签名的完整描述。如果这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码 |
attributes | 不同值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解, |
可以用 javap 查看编译后的 java class 文件字节码。如果你编译下面这个简单的类:
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println("Hello");
}
}
运行下面的命令,就可以得到下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。
public class org.jvminternals.SimpleClass
SourceFile: "SimpleClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public org.jvminternals.SimpleClass();
Signature: ()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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/jvminternals/SimpleClass;
public void sayHello();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String "Hello"
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/jvminternals/SimpleClass;
}
这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello 方法。
- **常量池:**提供了通常由符号表提供的相同信息,详细描述见下文。
- **方法:**每一个方法包含四个区域,签名和访问标签字节码 LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。
这个 class 文件用到下面这些字节码操作符:
aload0 | 这个操作码是 aload 格式操作码中的一个。它们用来把对象引用加载到操作码栈。表示正在被访问的局部变量数组的位置,但只能是 0、1、2、3 中的一个。还有一些其它类似的操作码用来载入非对象引用的数据,如 iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部变量数组位置大于 3 的局部变量可以用 iload, lload, float, dload 和 aload 载入。这些操作码都只需要一个操作数,即数组中的位置 |
---|---|
ldc | 这个操作码用来将常量从运行时常量池压栈到操作数栈 |
getstatic | 这个操作码用来把一个静态变量从运行时常量池的静态变量列表中压栈到操作数栈 |
invokespecial, invokevirtual | 这些操作码属于一组函数调用的操作码,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在这个 class 文件中,invokespecial 和 invokevirutal 两个指令都用到了,两者的区别是,invokevirutal 指令调用一个对象的实例方法,invokespecial 指令调用实例初始化方法、私有方法、父类方法, |
return | 这个操作码属于 ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组。每个操作码返回一种类型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void |
跟任何典型的字节码一样,操作数与局部变量、操作数栈、运行时常量池的主要交互如下所示。构造器函数包含两个指令。首先,this 变量被压栈到操作数栈,然后父类的构造器函数被调用,而这个构造器会消费 this,之后 this 被弹出操作数栈。
sayHello() 方法更加复杂,正如之前解释的那样,因为它需要用运行时常量池中的指向符号引用的真实引用。第一个操作码 getstatic 从 System 类中将 out 静态变量压到操作数栈。下一个操作码 ldc 把字符串 “Hello” 压栈到操作数栈。最后 invokevirtual 操作符会调用 System.out 变量的 println 方法,从操作数栈作弹出”Hello” 变量作为 println 的一个参数,并在当前线程开辟一个新栈帧。