我们在编辑器编辑的java代码不能直接执行,首先要编译生成class文件,然后将class文件加载到内存中,通过连接、解析和初始化,才可以被使用。
加载就是将类的.class文件中的二进制数据读入到内存中,将其放在运行时的数据区的方法区内,同时在堆区创建一个Java.lang.class对象,用来封装类在方法区中的数据结构,可以看到,不管我们创建多少个实例,class对象只有一个,在堆区。
graph LR
A(Java程序)-->|调用Class的方法比如newInstance|B(堆区内描述类的class对象)
B-->C(方法区中类的数据结构)
经过加载器进行加载,最终我们得到的是位于堆区的Class对象,Class对象封装了类在方法区的数据结构并且向Java程序开发者提供了访问方法区的数据结构的接口
类的加载不是开始全部加载,JVM有预判算法,在预料到某个类将要被使用的时候就进行预先加载,如果在预先加载过程中出现class损坏或不存在,则在类被首次主动使用的时候类加载器报LinkAgeError,(这个错误由JVM处理,我们一般接触不到)否则不报错。如果这个类一直没有被使用,那类加载器一直不报错。
类的连接是指将加载到内存中的类的二进制数据合并到虚拟机的运行环境中去。
确保被加载类的正确性,验证点如下:
1. 类文件结构的检查
java的类文件结构从上到下是包名,import,类,入口Main,方法/变量
2. 语义检查
确保语法正确,如Final类没有子类
3. 字节码验证
确保字节码流能够被JVM正确的执行
4. 二进制兼容性验证
两个由不同版本的jdk编译生成的class则有可能存在二进制不兼容的问题
在准备阶段,JVM为类的静态成员变量分配内存,并设置默认值。
//在准备阶段,下面这个Worker类,我们给age分配四字节内存,并赋予默认值 0
public Class Worker{
private static int age = 1;
static {
age = 100;
}
}
//byte是1字节内存,默认值是0
//short是2字节内存,默认值是0
//int是4字节内存,默认值是0
//long是8字节内存,默认值是0
//float是4字节内存,默认值是0.0
//double是8字节内存,默认值是0.0
//boolean是1字节内存,默认值是false
//char是1字节内存,默认值是'\u0000'
在解析阶段,JVM会将类的二进制数据中的符号引用转换成直接引用.如下:
public Class Worker{
public static int age = 0;
public static Car car;
public static void gotoWork(){
car.run();//这段代码在Worker类的二进制文件中表示为符号引用
}
}
在Worker类的二进制数据中,包含了对car的run方法的符号引用,他由run方法全名和操作符组成,在解析阶段,虚拟机会将符号引用转换成一个指针,该指针指向Car类run方法所在的内存位置,这个指针就是直接引用。
在初始化阶段,JVM为类的静态成员变成赋予正确的初始化值。
public Class Worker{
public static int age = 0;
static{
age = 100;
}
}
可以在声明变量的时候初始化,也可以在静态块中初始化。
graph LR
加载-->链接
链接-->验证
链接-->准备
链接-->解析
解析-->初始化
public Class Worker{
public static int age = 0;
public static Car car;
public static void gotoWork(){
car.run();
}
}
Worker worker = new Worker();
Worker.gotoWork();
Worker.age = 100;
Class clazz = Class.forName("***.***.Worker");
public Class WoodWorker extends Worker{
...
}
WoodWorker woodWorker = new WoodWorker();
//这个时候Worker就算是主动调用
//一般如果没有eclipse或者as,我们通常通过以下几步来编写代码
1.文本编写java文件
2.javac 命令生成class文件
3.java 入口类执行代码
在第三步,我们java Worker来执行Worker,那么Worker就算主动调用
public class TypicalCode {
private static TypicalCode typicalCode = new TypicalCode();
public static int a;
public static int b = 0;
// private static TypicalCode typicalCode = new TypicalCode();
//不同地方,测试结果会不相同
TypicalCode(){
a++;
b++;
}
public static TypicalCode getTypicalCode(){
return typicalCode;
}
}
//测试代码
@Test
public void test(){
TypicalCode typicalCode = TypicalCode.getTypicalCode();
System.out.println(typicalCode.a+"");
System.out.println(typicalCode.b+"");
}
//这就是在初始化阶段赋值导致的不同