本文目录
创建 HelloWorld.java
代码如下
package jvm;
/**
* @author lidiqing
* @since 2017/3/4.
*/
public class HelloWorld {
private final String text = "Hello World!";
public HelloWorld() {
}
public String getText() {
return text;
}
}
编译成 HelloWorld.class
结构:Class 文件 == 8位字节为单位的二进制流 == 无符号数 + 表
特点:
文件格式:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
魔数
唯一作用:确认该文件能被虚拟机接受
类加载过程中的验证阶段,会判断该 Magic
版本号和编译器 JDK 有关
magic
固定值:0xCAFEBABY
minor_version
次版本号:0x0000
major_version
主版本号:0x0034
Class 文件的版本表
编译器版本 | -target参数 | 十六进制版本号 | 十进制版本号(主版本号.次版本号) |
---|---|---|---|
JDK 1.7.0 | 不带(默认 -target 1.7) | 00 00 00 33 | 51.0 |
JDK 1.7.0 | -target 1.6 | 00 00 00 32 | 50.0 |
JDK 1.8.0 | 不带(默认 -target 1.8) | 00 00 00 34 | 52.0 |
特点:
主要类型:
jvm/HelloWorld
)jvm/Helloworld/text:Ljava/lang/String;
)java/lang/Object."":()V
)作用:
无论是字段表、方法表还是属性表,只要涉及到一些常量或者描述,都会有个索引指向该表的某个位置
constant_pool_count => 0x19
容量计数,常量表从 1 开始计数,所以 0x19 只有 0x18 个常量,十进制 24 个常量
这样的话,如果因为其他地方用了索引为 0,常量为空,比如 java.lang.Object 对象是所有类的基类,父索引为 0
constant_pool
对照 2.3.3 的常量池结果总表来进行分析,直接用 javap 输出 24 个常量如下
Constant pool:
#1 = Methodref #5.#20 // java/lang/Object."":()V
#2 = String #21 // Hello World!
#3 = Fieldref #4.#22 // jvm/HelloWorld.text:Ljava/lang/String;
#4 = Class #23 // jvm/HelloWorld
#5 = Class #24 // java/lang/Object
#6 = Utf8 text
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 ConstantValue
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Ljvm/HelloWorld;
#16 = Utf8 getText
#17 = Utf8 ()Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #9:#10 // "":()V
#21 = Utf8 Hello World!
#22 = NameAndType #6:#7 // text:Ljava/lang/String;
#23 = Utf8 jvm/HelloWorld
#24 = Utf8 java/lang/Object
Methodref,是方法的符号引用,比如虚拟机调用 invokespecial 指令会用到,比如该类的初始化方法字节码指令中有这么一条
1: invokespecial #1
这样会实例化 #1 的方法,而 #1 是 java.lang.Object 的初始化方法,所以该指令执行了 Object 的初始化
Fieldref,是字段的符号引用,比如虚拟机调用 putfield 指令会去解析该符号引用
7: putfield #3
这样的话会把该符号引用对应的变量,设置为栈帧中的值
NameAndType,字段或者方法的描述,有名称和描述符组合起来
Class,表示一个类的全限定名
String,表示一个字符串常量
Utf8,使用 utf-8 格式编码的字符串,最后其他类型的常量都会指向这些值,这是最基本的常量值
Class 文件加载后,常量池的字面量和符号引用会被存入方法区,多个线程共享。方法区又称为永久代,GC 基本不在方法区进行垃圾回收。但也会有,比如一些废弃的常量和无用的类
识别类或接口层次的访问信息
u2类型=2个字节=16位可用,目前只定义了8个,可见后面的访问标志表
在 class 中的二进制信息
该值为 0x21 = 0x01| 0x20
0x01 => ACC_PUBLIC
0x20 => ACC_SUPER
对照访问标志表,可知是一个普通的 public 类
使用 JDK 1.0.2 之后编译出来的类的 ACC_SUPER 均为真
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否public类型 |
ACC_FINAL | 0x0010 | 是否final类型,只有类可用 |
ACC_SUPER | 0x0020 | JDK1.2 之后必须为真 |
ACC_INTERFACE | 0x0200 | 标记是接口 |
ACC_ABSTRACT | 0x400 | 是否abstract类型,接口或抽象类为真,其他类为假 |
ACC_SYNTHETIC | 0x1000 | 标记类不是用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标记是注解 |
ACC_ENUM | 0x4000 | 标记是枚举 |
分为三种类型用来确定整个类的继承关系,属于类的元数据
根据规则,每个类只能有一个父类,但可以有多个接口
而更特殊的 java.lang.Object 是没有父类的,毕竟是所有类的基类,所以有且仅有 Object 类的父类索引为 0
在 class 中的二进制信息
this_class => 0x0004
这里指向常量池第4个常量,为 Class,全限定名为 jvm/HelloWorld
使用 javap 解析到的数据
Constant pool
#4 = Class #23
#23 = Utf8 jvm/HelloWorld
这个索引用来表示该类的全限定名
super_class => 0x0005
常量池第5个常量,为 Class,全限定名为 java/lang/Object
使用 javap 解析到的数据
Constant pool
#5 = Class #24
#24 = Utf8 java/lang/Object
这个索引用来表示该类父类的全限定名,所以该类的父类就是 java/lang/Object
interfaces_count => 0x0000
接口数量为 0
因为没有实现任何接口
interfaces
因为接口数量为 0,所以后面没有接口数据
范围:包含类级、实例级变量,不包括方法内部声明的局部变量
信息:
使用标记表示
用常量表示
fields_count => 0x0001
有一个字段
fields
从 fields_count 知道只有一个字段,查表可以得到 field_info 各个部分为:
access_flags => 0x0012=0x0010|0x0002
根据字段访问标志可知,这是个 private 和 final 型属性
name_index => 0x0006
索引对应常量池第6个常量,名为 “text”
Constant pool
#6 = Utf8 text
descriptor_index => 0x0007
索引对应常量池第7个常量,所以描述符为 “Ljava/lang/String;”
Constant pool
#6 = Utf8 Ljava/lang/String;
attribute_count => 0x0001
所以该字段有一个属性,接下来就是详细的 attribute_info 数据,表示该字段的属性
attribute_name_index => 0x0008
索引对应常量池第8个常量,属性名称为 “constantvalue”
Constant pool
#8 = Utf8 constantvalue
该属性用来通知虚拟机自动为静态变量赋值。这个发生在类的加载过程中的初始化阶段。关于类变量,初始化的时候会给默认值,但如果有 Constantvalue 属性,就会用这个属性的值来对类变量进行初始化
attribute_length => 0x00000002
constantvalue 的该值固定为 2,因为后面需要 u2 类型的数据来指向常量池,表示该 constantvalue 的值
constantvalue_index => 0x0002,索引对应常量池第2个常量,值为 “Hello World!”
Constant pool
#2 = String #21
#21 = Utf8 Hello World!
实际上,用代码进行比较的话,上面字段对应 Java 源码为
private final String text = "Hello World!";
在编译成 class 文件后,我们可以解析到这样的结果
flags: ACC_PRIVATE ACC_FINAL
name: text
descriptor: Ljava/lang/String;
attributes:
constantvalue:HelloWorld!
在 Java 中用 static 和 final 修饰的字段,即该类的常量字段。在 Java 编译阶段的常量传播优化中,如果 B 只引用了 A 的常量,编译后 A 的常量会被转化为 B 的常量,存入 B 类的常量池里。所以,常量传播优化后,使用者 B 就会拥有 A 的常量,对 A 常量的引用在 Class 文件中成为对自己的常量的引用
字段表(field_info):
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志(access_flag):
标志名称 | 标志值 | 含义 |
---|---|---|
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 | 是否 synthetic |
ACC_ENUM | 0x4000 | 是否 enum |
描述符(descriptor):
标识字符 | 含义 |
---|---|
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,如 Ljava/lang/Object |
数组每一维度用 “[” 来描述
比如
int[]
=> [I
string[][]
=> [[java/lang/String;
void inc
=> ()V
String toString
=> ()Ljava/lang/String;
int indexof(char[] a, int b, char[] c, int d)
=> ([CI[CI)I
类的字段和方法都由两部分来表示,即名称和描述符(NameAndType)
特点:
”和实例构造器“
”特征签名 => 一个方法中各个参数在常量池中的字段符号的引用集合,不包含返回值
methods_count => 0x0002
表示该类有两个方法
methods
分别有两个方法,都是 method_info 的结构。methods_count 紧接着就是第一个方法表
现在只分析第一个方法表
access_flag => 0x0001
方法为 Public 方法
name_index => 0x0009
方法名称为常量表第9个常量,是 Utf8 类型,为 “”,所以这个方法是类的初始化方法
Constant pool
#9 = Utf8
descriptor_index => 0x000A
方法描述符为常量表第10个常量,也是 Utf8 类型,为 “()V”
Constant pool
#10 = Utf8 ()V
attribute_count => 0x0001
属性数量为 1。从这个可知,该方法有一个属性。
基本上所有方法都至少有一个属性 “Code” 用来记录方法中编译后的字节码指令。也可以说,方法中的代码编译成字节码指令后都被存入了 “Code” 属性中了
attribute_name_index => 0x000B
常量表第11个常量,也是 Utf8 类型,就是 “Code” 属性
Constant pool
#11 = Utf8 Code
attribute_length => 0x0000003D
表示接下来的 61 个字节就是 “Code” 属性表的内容了,关于这个属性表的解析在后面
使用 javap 可以得出详细的解析结果如下:
方法表(method_info):
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法访问标志(access_flag):
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public |
ACC_PRIVATE | 0x0002 | 是否为 private |
ACC_PROTECTED | 0x0004 | 是否为 protect |
ACC_STATIC | 0x0008 | 是否为 static |
ACC_FINAL | 0x0010 | 是否为 final |
ACC_SYNCHRONIZED | 0x0020 | 是否为 synchronized |
ACC_BRIDGE | 0x0040 | 是否为编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 是否为 native |
ACC_ABSTRACT | 0x0400 | 是否为 abstract |
ACC_STRICTFP | 0x0800 | 是否为 strictfp |
ACC_SYNTHETIC | 0x1000 | 是否为编译器自动产生的 |
上面已经有用到两种属性,字段用到 constantvalue 用来表示是一个常量,方法用到了 code 来记录方法的字节码指令集合
特点:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
每个属性的名称都要从常量池中拿一个 Utf8 类型的常量。属性的内容为 attribute_length 个 info 组成,每个 info 由一个字节组成。所以属性的组成是很灵活的,如何解释这些 info,有虚拟机的标准,也可以完全自定义。但虚拟机在解析的时候,只取自己标准的那部分
使用在方法表中,用来表示代码编译成的字节码指令
具体的结构为
类型 | 名称 | 数量 |
---|---|---|
u2 | attibute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
现在拿类第一个方法表的 Code 属性来进行分析
attribute_name_index 和 attribute_length 上面已经解析过了,为 “Code”,长度为 29。即后面的 29 个字节均为该属性的内容
max_stack => 0x0002
操作数栈的最大深度,方法执行的任意时刻,操作数栈不会超过这个深度,虚拟机用这个值来分配栈帧(Stack Frame)中的操作数栈深度。这里表示该方法的操作数栈最深为 2。如果虚拟机执行的时候,有出现超出了这个深度,会抛出 Stack Overflow 的异常
max_locals => 0x0001
局部变量表所需的存储空间。该值的单位为 Slot,长度不超过 32 位的数据类型,比如 byte,int 等用一个 Slot,而 double 和 long 用两个 Slot。这里表示方法局部变量的存储空间为 1 个Slot
code_length => 0x0000000B
方法体内的字节码长度,这里的长度为 11,所以接下来 11 位就是字节码指令了
code
方法体的字节码指令,每个指令用一个 u1 表示,即 8 位,一共可以表达 256 条指令,目前虚拟机已经定义 200 多条了。code 指令可以分成以下几种类型。其中异常指令只记录显示抛出(throw)的异常,而像 try…catch 的异常则有属性表之异常表来处理
attributes_count => 0x00000002
表示后面还有两个属性,对照表可知为 LineNumberTable 和 LocalVariableTable。这两个属性都不是必须的,主要用来描述源码和字节码之间的一些关系
因为该方法比较简单,没有异常等。如果有异常,Code 属性中还会有异常表 Exception table
用处
LineNumberTable 具体的结构为
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_info 由 start_pc 和 line_number 组成,分别表示字节码行号和源码行号
现在拿上面的方法的 LineNumberTable 属性数据来进行分析
attribute_name_index => 0x000C
属性名称,指向常量池第 12 个常量,可以得知就是 “LineNumberTable”
Constant pool
#12 = Utf8 LineNumberTable
attribute_length => 0x0000000E
属性的长度为 15,所以接下来 15 个字节都是 LineNumberTable 属性的值
line_number_table_length => 0x0003
表示接下来有 3 个的 line_number_info 类型,用来表示字节码和源码的对应关系。有可能多个字节指令才会对应一条源码
line_number_table
现在就分析第一个 line_number_info 的信息
start_pc => 0x0000,字节码第0行
PC 的全称为 Program Counter,即程序计数,对应程序计数器的值,表示在字节码指令中的偏移量。因为要保证线程切换后能回到正确的位置,所以每个线程都会有一个程序计数器
line_number => 0x000B,源码第11行
结合 start_pc 可知,所以 0:aload_0
指令对应的源码为 public HelloWorld() {
使用 javap 解析到方法 “” 的完整 LineNumberTable 表如下
有了这些class文件中有了这些信息,我们就可以进行断点调试了。所以断点调试是依赖于方法表中是否有设置 LineNumberTable 属性
用处
LocalVariableTable 具体的结构为
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info 的结构为
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
用第一个方法表的 code 属性的 LocalVariableTable 属性数据来分析
attribute_name_index => 0x000D
指向常量池第 13 个常量,可以得知就是 “LocalVariableTable”
attribute_length => 0x0000000C
属性的长度为 12,所以接下来 12 个字节都是 LocalVariableTable 属性的值
local_variable_table_length => 0x0001
表示接下来有一个 local_variable_info 用来表示栈帧内局部变量表和源码的关系
local_varibale_info
start_pc => 0x0000
该局部变量的生命周期开始的字节码偏移量
length => 0x000B
该局部变量的生命周期作用范围的长度
所以该局部变量作用在 0 -> 11 条字节码之间,可知为整个方法的字节码指令集范围内
name_index => 0x000E
指向常量池第 14 个常量,为 “this”,所以这个局部变量是 this 指针,指向使用该方法的当前实例
descriptor_index => 0x000F
指向常量池第 15 个常量,为 “Ljvm/HelloWorld
”,所以这个 this 指针代表的就是类 HelloWorld 的实例
index => 0x0000
表示该局部变量在栈帧局部变量表中 Slot 的位置,可知为第一个 Slot
该属性在 2.6.2 对字段 “text” 的解析中已经提到,具体的作用是通知虚拟机初始化静态变量
目前类变量的初始化有两种方式
中