类从被加载到虚拟机,到被卸载。其整个生命周期包括以上几个阶段:加载 (Loading)、验证 (Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using)、卸载 (Unloading)
。java
其中类加载过程包括加载 (Loading)、验证 (Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)
五个阶段。除了解析 (Resolution)
阶段顺序不肯定之外,其余四个阶段是按顺序开始的。数据库
解析阶段可能在初始化阶段以后开始,其目的是为了支持Java的动态绑定,好比多态(调用父类方法实际执行的是子类覆盖的方法)。安全
在加载阶段,虚拟机完成如下三件事情:网络
java.lang.Class
对象,做为对方法区中这些数据的访问入口。在加载阶段咱们能够实现本身的ClassLoader
,从而能够动态的建立符合特定化需求的类,或者是能够从特定的数据源 (网络、文件系统、数据库等等) 获取class文件。数据结构
大体的类加载器层级结构以下,多线程
启动类加载器 (Bootstrap ClassLoader),负责加载 JDK\jre\lib 下
,或被-Xbootclasspath
参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器没法被Java程序直接引用。布局
扩展类加载器 (Extension ClassLoader),负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器。spa
应用程序类加载器 (Application ClassLoader), 负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器。若是没有自定义的类加载器,通常这就是程序中默认的类加载器。线程
如上图所示的这种层级结构成为Java类加载器的双亲委派模型。当前加载器上层的叫作父加载器,但他们的关系不是靠继承来实现,而是使用组合的方式。设计
若是一个类加载器收到加载类的请求,首先将请求委托给父加载器,依次向上层请求,直到顶端的启动加载器,此时只有当该加载器找不到对应的类时,才会让子类去加载,直到找到该类。
Java设计者提出的这种约束模型,
例如要请求加载java.lang.Object
类,最终全部的加载器都会委托启动加载器去加载,从而保证了不管是哪个加载器想要加载Object
类,最终都指向同一个类。
验证是为了确保Class文件中的字节流符合虚拟机的要求,而且不会损害虚拟机安全。
验证大体分为四个阶段,
主要为类变量分配内存并设置变量初始值,都在方法区中进行分配。
注意要点:
举个例子,以下
public static int number = 6;
复制代码
变量number
在准备阶段后的值为0,而不是6。由于此时尚未开始执行Java方法,而将变量number
赋值为6是在程序编译后,执行putstatic
指令,存放于类的构造器<clinit>()
方法中,因此number
赋值为6的操做将在初始化阶段进行。
但若是上述变量前加上final
关键字,则会在编译期就将其结果放入常量池中,即准备阶段后此时number
值为6.
该阶段虚拟机完成将常量池中符号引用转化为直接引用的过程。其中,
解析主要针对类或接口、字段、类方法、接口方法四类符号引用,他们和常量池中的类型对应关系以下表,
符号引用 | 常量类型(常量池) |
---|---|
类或接口 | CONSTANT_Class_info |
字段 | CONSTANT_Fieldref_info |
类方法 | CONSTANT_Methodref_info |
接口方法 | CONSTANT_InterfaceMethodref_info |
此阶段开始执行类中的Java代码,主要执行的是类构造器<clinit>()
方法。
<clinit>()
是由编译器自动收集类中全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序由 语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它以前的类变量,定义在它以后的类 变量只能赋值,不能访问。例如如下代码:
public class Test {
static {
i = 0; // 给变量赋值能够正常编译经过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
复制代码
因为父类的 <clinit>()
方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如如下代码:
static class Parent {
public static int A = 1; static { A = 2; }
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
复制代码
接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成 <clinit>()
方法。但 接口与类不一样的是,执行接口的 <clinit>()
方法不须要先执行父接口的 <clinit>()
方法。只有当父接口中定义的变量使 用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()
方法。
虚拟机会保证一个类的 <clinit>()
方法在多线程环境下被正确的加锁和同步,若是多个线程同时初始化一个类,只会 有一个线程执行这个类的 <clinit>()
方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>()
方法完毕。若是在一 个类的 <clinit>()
方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。