类加载与字节码技术
1. 类文件结构
2. 字节码指令
3. 编译期处理
4. 类加载阶段
5. 类加载器
6. 运行期优化
![](https://i-blog.csdnimg.cn/blog_migrate/cead8282a96f38759a15fe93df571b07.png)
1. 类文件结构
类文件样子:
有哪些东西:
魔数(magic):表示class类型的文件
版本(version):表示类的版本,java8
常量池:常量池长度,method信息,field信息,class信息等
访问标识与继承信息
field信息:成员变量
method信息:方法
附加属性
2. 字节码指令
方法执行流程:
1、jvm把main方法所在的类进行类加载操作,把.class里的字节码读取到内存中
2、.class常量池中的数据载入运行时常量池(方法区的组成部分)
注:比较小的数字不会存储在常量池中,会和字节码指令存在一起。如果超过一定最大值(整数最大值),就会存储在常量池中
3、方法字节码载入方法区
4、main线程开始运行,分配栈帧内存
5、执行引擎开始执行字节码
借助局部变量表和操作数栈
2.1构造方法(前面还有一些字节码指令,我觉这个比较重要)
1)<cinit>()V :整个类的构造方法
public class Demo3_8_1 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V
<cinit>()V 方法会在类加载的初始化阶段被调用
2) <init>()V :某个实例的构造方法
public class Demo3_8_2 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo3_8_2(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_8_2 d = new Demo3_8_2("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,
但原始构造方法内的代码总是在最后
2.2方法调用
public class Demo3_9 {
public Demo3_9() {
}
private void test1() {
}
private final void test2() {//final修饰的方法无论是私有还是公有对应的字节码指令都一样
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
- 最终方法(fifinal),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
- 普通成员方法(public)是由 invokevirtual 调用,属于动态绑定,即支持多态,在编译期间不能确定调用那个对象的方法(父类还是子类。。),在运行时确定
- 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
- 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了😂
- 还有一个执行 invokespecial 的情况是通过 super 调用父类方法
2.3finally
finally 中的代码被复制了
3 份,分别放入
try 流程,
catch 流程以及
catch 剩余的异常类型流程
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
如果finally语句中有return,会吞掉异常,不会抛出,如上代码
2.4synchronized
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
注:方法级别的 synchronized 不会在字节码指令中有所体现 public synchronized String test(){}
3. 编译期处理
语法糖:
其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
3.1 默认构造器
3.2 自动拆装箱
jdk5开始加入
3.3 泛型集合取值
java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
3.4 可变参数
可变参数 String... args 其实是一个 String[] args
3.5 foreach 循环
增强for循环
注:foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator )
3.6 switch 字符串
3.7匿名内部类
4. 类加载阶段
4.1 加载
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 fifield 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fifields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注:instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
4.2 链接
验证
验证类是否符合 JVM规范,安全性检查
准备
为 static 变量分配空间,设置默认值
- 1.7开始存储在堆中
-
static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
-
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
-
如果 static 变量是 final 的,但属于引用类型(new对象),那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
4.3 初始化
<cinit>()V 方法,类的初始化方法,初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
5. 类加载器
6. 运行期优化
6.1 即时编译
分层编译
方法内联