一、加载
加载是查找并加载类的二进制数据
把类的.class文件中的二进制数据读入到内存中,把它放在运行时数据区的方法区中,然后在堆区创建一个Class对象,用于封装类在方法区内的数据结构。
Class对象封装了类在方法区内的数据结构,并且向Java程序提供访问类在方法区的数据结构接口。
Java程序 - - - -(调用Class对象方法)- - - - > 堆中:描述Dog类的Class对象 - - - - - - - - > 方法区:Dog类的数据结构
类的加载是由加载器完成的,加载器有:启动类加载器、扩展类加载器、系统类加载器、用户自定义类加载器。
类加载器并不需要等到某个类被“首次主动使用”时才加载它,Java虚拟机规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在加载的过程中遇到.class文件缺失或者存在错误,类加载器必须等到首次主动使用该类时才报告错误,如果这个类一直没有被程序主动使用,那么类加载器将不会报告错误。
二、连接
连接就是把已经读到内存的二进制数据合并到虚拟机运行环境中去
连接阶段包括:验证、准备、解析
验证:确保加载类的正确性,例如,类文件的结构检查、语义检查、final类型的类有没有子类、字节码验证等;
准备:为类的静态变量分配内存,并设置默认的初始值,例如,
//Dog类有静态变量a
private static int a = 1;//在准备阶段,将为a分配4字节的内存空间,并赋予默认值0;
解析:把类中的符号引用转换为直接引用,例如,
/**
* 在当前类的二进制数据中,包含了一个对Dog类的run()方法的符号引用,它有run()方法的全名和相关描述
* 符组成。
* 在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Dog类的run()方法在方法区内
* 的内存位置,
* 这个指针就是直接引用。
*/
public void goRun(){
dog.run(); //这段代码在worker类的二进制数据中表示为符号以引用
}
三、初始化
初始化是为静态变量赋予正确的初始值(此处不是指常量,Java编译器和虚拟机对常量有特殊的处理方式)
Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值,静态变量初始化有两种途径:
(1)在静态变量的声明出进行初始化;
(2)在静态代码块进行初始化。
假如类存在父类,并且这个父类还没有被初始化,那就先初始化父类。
在类被加载和连接的时机上,Java虚拟机提供了一定的灵活性,但是它严格定义了初始化的时机,所有的Java虚拟机实现必须在每个被Java程序“首次主动使用”时才初始化他们。
Java程序对类的使用方式可分为两种:主动使用(导致类的初始化)、被动使用(不会导致类的初始化)
只有6种活动别看做程序对类的主动使用:
(1)创建类的实例,通过new、反射、克隆、反序列化手段来创建实例
(2)调用类的静态方法
(3)访问类的静态变量
(4)调用某些反射方法,forName()方法
(5)初始化一个类的子类
(6)被表明启动类的类
栗子:
(1)对于final类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看做编译时常量,如下面的栗子,当Java编译器生成Test类的.class文件时,它不会在main()方法的字节流中保存一个表示Dog.b的符号引用,而是直接在字节流中嵌入常量6,因此当程序访问Dog,b时,客观上无须初始化Dog。
如果在编译时就不能计算出变量的取值,那么程序对这种变量的使用,被看做对类的主动使用。
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// int a = Dog.a; //输出"Dog static"
int b = Dog.b; //没有输出
// int c = Dog.c; //输出"Dog static"
}
}
class Dog{
// 访问类的静态变量,被看做类的主动使用,会导致类的初始化
public static int a = 20;
// 访问类的编译时常量,被看做类的被动使用,不会导致类的初始化
public static final int b = 2*3;
// c不是编译时常量,被看做类的主动使用,会导致类的初始化
public static final int c = (int)Math.random();
static {
System.out.println("Dog static");
}
}
(2)当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
(3)只有当程序访问的静态变量或静态方法的确在当前类定义时,才可以看做对类的主动调用。
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
int a = Dog.a; //输出 "Animal static"
// int b = Dog.b; //输出 "Animal static" "Dog static"
}
}
class Dog extends Animal{
public static int b = 20;
static {
System.out.println("Dog static");
}
}
class Animal{
public static int a = 10;
static {
System.out.println("Animal static");
}
}
(4)调用ClassLoader类的loadClass方法加载一个类,并不会对类的主动使用,不会导致类的初始化;调用Class类的forName()方法,是对类的主动使用,会导致类的初始化。
package com.example.object;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// classLoader.loadClass("com.example.object.Dog"); //无输出
Class.forName("com.example.object.Dog"); //输出 "Dog static"
}
}
class Dog {
public static int a = 10;
static {
System.out.println("Dog static");
}
}
四、卸载
当类的Class对象不再被引用,Class对象就会结束生命周期,方法区的数据也会被卸载。
有Java虚拟机自带的类加载器,所加载的类,在虚拟机的生命周期中,始终不会被卸载,Java虚拟机本身会始终引用这些类加载器,而这些类加载器则始终引用它们锁加载类的Class对象。
当类处于生命周期中时:类二进制数据位于方法区中;在堆内会有一个相应的描述这个类的Class对象。