三、类加载与字节码技术
1、类文件结构
1.1 魔数
u4 magic
对应字节码文件的0~3个字节
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
1.2 版本
- minor_version;
- major_version;
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
34H = 52,代表JDK8
1.3 常量池
用作引用
1.4 访问标识与继承信息
是否是公共
父类/接口
1.5 Field信息
表示成员变量
1.6 Method信息
表示方法
1.7 附加属性
2、字节码指令
- aload_0 加载slot 0 的局部变量
- Invokespecial 调用构造方法
- Getstatic 加载静态变量
- Ldc 加载参数
- Invokevirtual 调用成员方法
2.1 入门
2.2 Javap工具
javap 工具来反编译 class 文件
2.3 图解方法执行流程
(1)原始java代码
(3)常量池载入运行时常量池
(4)方法字节码载入方法区
(5)main线程开始运行,分配栈内存
(stack=2,locals=4)
(6)执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore_1
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1
ldc #3
- 从常量池加载 #3 数据到操作数栈
- 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore_2
iload1 iload2
- 将局部变量表中1号位置和2号位置的元素放入操作数栈中
- 因为只能在操作数栈中执行运算操作
iadd
- 将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中
istore 3
- 将操作数栈中的元素弹出,放入局部变量表的3号位置
getstatic #4
- 在运行时常量池中找到#4,发现是一个对象
- 在堆内存中找到该对象,并将其引用放入操作数栈中
iload 3
- 将局部变量表中3号位置的元素压入操作数栈中
invokevirtual 5
- 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
2.4 练习I++
略
2.5 条件判断指令
略
2.6 循环控制指令
略(while, do-while,for)
2.7 练习--判断结果
2.8 构造方法
2.9 方法的调用
2.10 多态的原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令。在执行invokevirtual指令时,经历了以下几个步骤。
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有vtable
- 查询vtable找到方法的具体地址
- 执行方法的字节码
2.11 异常处理
2.12 finally
2.13 synchronized
3、编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换**的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记
3.1 默认构造器
3.2 自动拆箱器
3.3 泛型集合取值
3.4 可变参数
3.5 foreach循环
3.6 switch字符串
过程说明:
- 在编译期间,单个的switch被分为了两个
- 第一个用来匹配字符串,并给x赋值
- 字符串的匹配用到了字符串的hashCode,还用到了equals方法
- 使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.)
- 第二个用来根据x的值来决定输出语句
3.7 switch枚举
3.8 枚举类
3.9 try-with-resources
略
3.10 方法重写时的桥接方法
3.11 匿名内部类
4、类加载阶段
4.1 加载
将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的加载
instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
_java_mirror则是保存在堆内存中
InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址
类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
4.2 链接
(1)验证
验证类是否符合 JVM规范,安全性检查
(2)准备
为 static 变量分配空间,设置默认值
- static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
- static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
(2)解析
解析的含义:
将常量池中的符号引用解析为直接引用。未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中。
4.3 初始化
4.4 练习
5、类加载器
5.1 启动类加载器
可通过在控制台输入指令,使得类被启动类加器加载
5.2 拓展类加载器
如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载
5.3 双亲委派模式
5.4 线程的上下文类加载器
JDBC 时,都需要加载 Driver 驱动
(1)SPI
(2)最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
5.5 自定义类加载器
6、运行期优化
6.1 即时编译
(1) 分层编译
JVM 将执行状态分成了 5 个层次:
0层:解释执行,用解释器将字节码翻译为机器码
1层:使用 C1 即时编译器编译执行(不带 profiling)
2层:使用 C1 即时编译器编译执行(带基本的profiling)
3层:使用 C1 即时编译器编译执行(带完全的profiling)
4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别
解释器
将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
是将字节码解释为针对所有平台都通用的机器码
即时编译器
将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码
(2)方法内联
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换
(3)字段优化
6.2 反射优化
本质上将反射调用,变为正常调用
• 加载(本质是用IO将文件加载)
-
- Bootstrap 核心库
-
- Extension 拓展库
-
- System Classloader 系统的类路径
-
- 自定义
• 链接
将Java类的二进制代码合并到JVM的运行状态之中。
• 验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
• 准备
为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如
public static int value = 123;
此时在准备阶段过后的初始值为0而不是123;
特例:
public static final int value = 123;
此时value的值在准备阶段过后就是123。
• 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程
• 初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始,从上到下顺序进行。
-
- New 指令
-
- Reflect 反射方法
-
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。