1.类加载的时机
下图是类的生命周期,这里的类既指类class也指接口interface:
其中,加载、验证、准备、初始化和卸载这五个阶段是按顺序开始的,解析则不一定,可能在初始化开始之后才开始。这里说“开始”是因为这些阶段可能是互相交叉地进行的。
下面五种情况必须立即对类进行初始化(加载、验证、准备阶段自然要在之前进行):
下面三个例子是不会触发初始化的:
public class SuperClass { static { System.out.println ("SuperClass init"); } public static value = 123; } public class SubClass extends SuperClass { static { System.out.println ("SubClass init"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); } } //output: //SuperClass init
对于静态字段,只有直接定义这个字段的类才会被初始化。至于子类是否被加载和验证,则不同虚拟机会有不同的实现。
public class NotInitialization { public static void main(String[] args) { SuperClass[] sca = new SuperClass[10]; } } //no output
并没有过触发“com...SuperClass”的初始化,而是触发“[Lcom...SuperClass”的初始化阶段,这是由虚拟机自动生成的类,创建动作有字符码newarray触发。
public class ConstClass { static { System.out.println("ConstClass init"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } } //no "ConstClass init" output
编译阶段已经通过常量传播优化,将常量的值存储到NotInitialization的常量池中。
2.类加载过程
在加载阶段,虚拟机要完成三件事情:
1) 通过类的全限定名获取此类的二进制字节流
2) 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3) 在内存中生成一个代表这个类的Class对象,作为方法区中这个类各种数据的访问入口
验证是连接阶段的第一步,目的是为了确保Class文件中的字节流所包含的信息符合虚拟机要求而且不会危害虚拟机自身安全。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这里说的内存分配仅包括类变量,不包括实例变量,而设置初始值指的是设置零值。
解析过程是将常量池内的符号引用替换成直接引用的过程。
到了初始化阶段才真正开始执行类中的Java代码。初始化阶段执行类构造器<clinit>()方法。
<clinit>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生,收集顺序是由原文件出现的顺序决定的。静态语句块只能访问定义在其前面的变量,赋值则没有限制。
<clinit>()方法与构造方法不同,它不需要显示地调用父类的构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
<clinit>()方法不是必需的,如果没有静态代码块也没有对变量的赋值操作,可以不生成<clinit>()。
虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确的加锁和同步。
3.类加载器
类加载器是用来完成“通过类的全限定名获取此类的二进制字节流”这个过程的。