一 类加载的时机
1.类的主动引用(一定会发生类的初始化)
(1)new一个类的对象;
(2)使用java.lang.reflect包的方法对类进行反射调用;
(3)调用类的静态成员(除了final常量)和静态方法;
(4)当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
(5)当初始化一个类,如果其父类没有被初始化,则先初始化他的父类。
2.类的被动引用(不会发生类的初始化)
(1)通过子类引用父类的静态字段,不会导致子类初始化;
(2)通过数组定义来引用类,不会触发此类的初始化;
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;
二 类加载的过程
1.加载
将Class文件字节码加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
2.验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
验证大致分为四个动作:文件格式验证,元数据验证,字节码验证,符号引用验证。
3.准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
注意:
(1)进行内存分配的仅包括类变量(static变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;
(2)初始值通常情况下是数据类型的零值。eg:
public static int value = 123;
这里变量value在准备阶段过后的初始值为0而不是123,因为这时候还没开始执行任何的Java方法,而把value赋值为123的指令是程序被编译后,存放在类构造器方法中的,所以把value赋值为123的动作在初始化阶段才会执行。
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据该属性设置将value赋值为123。
4.解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
5.初始化
初始化阶段是执行类构造器<clinit>()方法的过程。
(1)类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
(2)当初始化一个类的时候,如果发现其父类还没有初始化,则需要先对其父类进行初始化。
(3)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步。
eg:
public class Demo01{
static{
System.out.println("类Demo01静态块");
}
static final int x = 123;
public static void main(String[] args){
A a = new A();
}
}
class A{
static final int y = 456;
static{
System.out.println("类A静态块");
}
}
三 类加载器
类加载器用于实现类的加载动作
类加载器的分类:启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器
类加载器双亲委派模型:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。类加载器之间的父子关系一般不会以继承的关系来实现,而是都是用的组合关系来复用加载器的代码。
双亲委派模型的工作过程:如果一个类加载器受到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。