JVM-从字节码到运行时(1)
一切从javap -verbose开始
希望借此文章将Class文件结构和运行时的知识点串联起来
先来段代码:
public class ByteCodeDemo {
private String s = "abc";
private int a = 100;
private final static int b = 200;
public int add(int z) {
int c = 300;
return (a + b + c) * z;
}
public static int getB() {
return b;
}
}
使用javap -verbose反编译Class文件得出以下Class文件结构:
Last modified 2021-1-18; size 427 bytes
MD5 checksum b811f62df11dd920ae644458a9c24459
Compiled from "ByteCodeDemo.java"
public class ByteCodeDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // abc
#3 = Fieldref #5.#26 // classone/ByteCodeDemo.s:Ljava/lang/String;
#4 = Fieldref #5.#27 // classone/ByteCodeDemo.a:I
#5 = Class #28 // classone/ByteCodeDemo
#6 = Class #29 // java/lang/Object
#7 = Utf8 s
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 a
#10 = Utf8 I
#11 = Utf8 b
#12 = Utf8 ConstantValue
#13 = Integer 200
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 add
#19 = Utf8 (I)I
#20 = Utf8 getB
#21 = Utf8 ()I
#22 = Utf8 SourceFile
#23 = Utf8 ByteCodeDemo.java
#24 = NameAndType #14:#15 // "<init>":()V
#25 = Utf8 abc
#26 = NameAndType #7:#8 // s:Ljava/lang/String;
#27 = NameAndType #9:#10 // a:I
#28 = Utf8 classone/ByteCodeDemo
#29 = Utf8 java/lang/Object
{
public classone.ByteCodeDemo();
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: ldc #2 // String abc
7: putfield #3 // Field s:Ljava/lang/String;
10: aload_0
11: bipush 100
13: putfield #4 // Field a:I
16: return
LineNumberTable:
line 3: 0
line 5: 4
line 6: 10
public int add(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: sipush 300
3: istore_2
4: aload_0
5: getfield #4 // Field a:I
8: sipush 200
11: iadd
12: iload_2
13: iadd
14: iload_1
15: imul
16: ireturn
LineNumberTable:
line 10: 0
line 11: 4
public static int getB();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: sipush 200
3: ireturn
LineNumberTable:
line 15: 0
}
Class文件结构
根据《JAVA虚拟机规范》中定义,Class文件结构采用一种C语言结构体的伪结构来存储数据。这种伪结构中包括两种类型:无符号数和表。
- 无符号数属于基本数据类型,表示长度:u1为1个字节,u2为2个字节,u4为4个字节,u8为8个字节。
- 表是用于描述有层次关系的复合数据结构,数据项按严格的结构顺序组成。
(此处应有图)
结合图中所示的数据项和例子中反编译的Class文件内容,按顺序进行解析:
minor version: 0 //次版本号
major version: 52 //主版本号
主版本号与次版本号组合起来主版本号.次版本号
表示编译此Class文件的jdk版本号。在这个例子里,java文件是由版本52.0来编译的,也就是JDK8。
flags: ACC_PUBLIC, ACC_SUPER //access_flags,访问标识
flags表示该类的访问标志,其中
- ACC_PUBLIC 表示该类为
PUBLIC
类型 - ACC_SUPER 是否允许使用
invokespecial
字节码指令,JDK1.0.2之后该值都为真
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // abc
#3 = Fieldref #5.#26 // classone/ByteCodeDemo.s:Ljava/lang/String;
#4 = Fieldref #5.#27 // classone/ByteCodeDemo.a:I
#5 = Class #28 // classone/ByteCodeDemo
#6 = Class #29 // java/lang/Object
#7 = Utf8 s
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 a
#10 = Utf8 I
#11 = Utf8 b
#12 = Utf8 ConstantValue
#13 = Integer 200
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 add
#19 = Utf8 (I)I
#20 = Utf8 getB
#21 = Utf8 ()I
#22 = Utf8 SourceFile
#23 = Utf8 ByteCodeDemo.java
#24 = NameAndType #14:#15 // "<init>":()V
#25 = Utf8 abc
#26 = NameAndType #7:#8 // s:Ljava/lang/String;
#27 = NameAndType #9:#10 // a:I
#28 = Utf8 classone/ByteCodeDemo
#29 = Utf8 java/lang/Object
Class文件常量池
Constant pool
表示常量池入口。常量池中主要存放两大类常量:字面量
和符号引用
。
- 字面量接近于常量概念,比如字符串、被声明为
final
的常量值等。 - 符号引用则属于编译原理方面的概念,主要包括以下几类常量:
- 被模块导出或者开放的包
- 类和接口的全限定名
#5 = Class #23 // ByteCodeDemo
#6 = Class #24 // java/lang/Object
- 字段的名称和描述符
// 字符串常量
#2 = String #25 // abc
// 字段引用
#3 = Fieldref #5.#26 // classone/ByteCodeDemo.s:Ljava/lang/String;
#4 = Fieldref #5.#27 // classone/ByteCodeDemo.a:I
// 字段名称
// 字段类型
#7 = Utf8 s
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 a
#10 = Utf8 I
// 被final修饰的字段
#11 = Utf8 b
#12 = Utf8 ConstantValue
#13 = Integer 200
- 方法的名称和描述符
方法名+返回值类型
#18 = Utf8 add
#19 = Utf8 (I)I
#20 = Utf8 getB
#21 = Utf8 ()I
- 方法句柄和方法类型
- 动态调用点和动态常量
上面说到
Constant pool
表示常量池入口。
这里为什么说是入口呢,因为Class文件里,可以理解为保存的是静态的内容,而各个方法、字段的符号引用要在类加载时才得到真正的内存入口。相对的,动态的内容,就指的是运行时常量池了。
运行时常量池
运行时常量池,存放的是Class文件中的常量池经过类加载后的内容,存放这部分内容的地方就是Metaspace
。这里提到类加载,先简单和类的加载过程关联起来:类加载过程中的解析阶段,先从Class文件的常量池中获取符号引用,经过解析后替换为直接引用。而直接引用,就是可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄,简单点来说,就是对象的访问定位。
常量池小结
常量池一般包括两部分:
- 静态常量池,指的是Class文件结构中的常量池。
- 运行时常量池,指的是由静态常量池经过运行时的加载转变的。
还可以推断出:
- 符号引用属于静态数据,直接引用属于运行时数据
- Class文件结构中存放的是符号引用,运行时常量池中存放的是直接引用
网友评论