类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态数据结构转化为方法区中运行的数据结构,并且在堆内存中生成一个java.lang.Class对象作为访问方法区数据结构的入口
类加载的最终产物是堆内存中的Class对象,对于同一个ClassLoader来说,无论类被加载多少次,对应带堆内存中的对象始终是同一个,这里所说的加载是类加载过程中的第一个阶段,第二个阶段
连接阶段:
1.验证,确保当前class文件的字节流所包含的内容符合当前JVM的规范要求,并且不会出现危害JVM自身安全的代码,当前字节流不符合规范会抛出VerifyError的异常,或者子异常,验证的信息有:(1)文件格式:验证二进制文件是什么类型,验证是否符合当前JVM规范,(2)元数据验证:检查类是否有父类、接口,验证其父类、接口的合法性, 验证被final修饰的类, 验证是否是抽象类,是否实现了父类的抽象方法或者接口中的方法, 验证方法的重载。(3)字节码验证,主要验证程序的控制流程比如循环、分支等,(4)符号验证,主要验证符号引用转换为直接引用时的合法性
2.准备,当一个Class文件的字节流通过验证,就开始为该对象的类变量,也就是静态变量,分配内存和初始值,各种数据类型的初始值:
序号 | 数据类型 | 大小/位 | 封装类 | 默认值 | 可表示数据范围 |
1 | byte(位) | 8 | Byte | 0 | -128~127 |
2 | short(短整数) | 16 | Short | 0 | -32768~32767 |
3 | int(整数) | 32 | Integer | 0 | -2147483648~2147483647 |
4 | long(长整数) | 64 | Long | 0L | -9223372036854775808~9223372036854775807 |
5 | float(单精度) | 32 | Float | 0.0f | 1.4E-45~3.4028235E38 |
6 | double(双精度) | 64 | Double | 0.0 | 4.9E-324~1.7976931348623157E308 |
7 | char(字符) | 16 | Character | '/uoooo'(null) | 0~65535 |
8 | boolean | 8 | Boolean | flase | true或false |
final修饰:
private static int aa = 10;//(1)
private static final int bb = 10;//(2)
在(1)的位置 static int aa = 10在准备阶段中不是10,而是初始值0,而(2)static final int bb= 10会是10,因为final修饰的静态变量不会导致类的初始化,可以直接计算出结果。
3.解析,所谓解析就是指在常量池中找到类、接口、方法、字段的符号引用,并将其替换为直接引用的过程。
4.初始化,执行<clinit>()方法(clinit是class initialize的简写),<clinit>()方法再编译过程中生成,此方法中包含了所有类变量的赋值以及静态代码语句块的执行代码,编译器收集的顺序是由执行语句在源文件中的出现顺序来决定的,静态语句块只能对后面的静态变量进行赋值,而不能对其进行访问,
public class test2 {
static {
System.out.println(X);(1)
X =100;
}
private static int X = 10;
}
在(1)位置,会编译错误,illegal forward reference,另外,父类的静态语句块会被优先执行java编译器会生成<clinit>()方法,但是该方法不是每一个类都会生成,如果某一个类无静态变量或者静态语句块,就没有生成<clinit>()方法的必要,<clinit>()方法在触发了类的初始化是被调用,如果有多个线程同时访问这个方法,不会引起线程安全问题,
package ClassTest;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class ClassInit {
static{
try {
System.out.println("这是静态语句块,我被初始化了");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
IntStream.range(0,5).forEach(i->new Thread(ClassInit :: new));
}
}
运行结果为
这是静态语句块,我被初始化了
休眠结束
Process finished with exit code 0
在同一时间,只能有一个线程执行到静态代码块中的内容,并且静态代码块仅仅执行一次,JVM保证了<clinit>()方法在多线程的执行环境下的同步语义。
再看一个小栗子
package ClassTest;
public class Singleton {
//(1)
private static int x = 0;
private static int y ;
private static Singleton singleton = new Singleton();//(2)
private Singleton(){
x++;
y++;
}
public static Singleton getSingleton(){
return singleton;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
System.out.println("这是X变量"+singleton.x);
System.out.println("这是Y变量"+singleton.y);
}
}
当(2)语句放到(1)位置时,输出的结果会有变化,目前这个代码输出结果会是:X=1,Y=1。当语句变动后,输出结果为:X=0,Y=1。解释一下:
private static int x = 0;
private static int y ;
private static Singleton singleton = new Singleton();
在连接阶段的准备过程中,每个变量都会被赋一个初始值 x=0 y=0 singleton = null,跳过解析过程,在初始化阶段每一个变量赋正确的值
x=0,y=0,singleton=new Singleton(),在new Singleton的时候执行类的构造函数,对x和y进行了自增,因此结果为x=1,y=1.
private static Singleton singleton = new Singleton();
private static int x = 0;
private static int y ;
在连接阶段的准备过程中,每个变量都会被赋一个初始值 singleton = null,x=0,y=0 跳过解析过程,在初始化阶段每一个变量赋正确的值,首先会进入Singleton的构造函数,执行完构造函数后 x=1,y=1,然后开始初始化x,y, x被赋予0的值,y没有被赋值,因此在构造函数中的值即为y的值,y=1,所以结果为x=0,y=1