类的生命周期
加载
在加载阶段,虚拟机主要完成以下三件事
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将二进制字节流代表的静态存储结构转换为方法区(hotspot 是把 class 对象存放在方法区中的)的运行时数据结构。
- 生成这个类的
java.lang.Class
的对象。
验证
确保 Class 文件中的字节流中办函的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这个阶段大致会验证以下 4 个部分。
- 文件格式验证 验证字节流是否符合 Class 文件的规范,并且能够被当前版本的虚拟机处理。
- 元数据验证 对字节码描述的信息进行语言分析,保证其描述的信息符合 Java 语言规范。
- 字节码验证 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证 这个验证发生在虚拟机将符号引用转化为直接引用的时候。
准备
这个阶段是正式为类变量(static 变量)分配类型并设置类变量初始值的阶段。
解析
解析是虚拟机将常量池的符号引用转换为直接引用的过程。
初始化
到了初始化阶段才真正执行 Java 的字节码。初始化过程是执行类构造器<clinit>()
方法的过程。<clinit>()
方法是由编译器自动收集类变量的赋值动作和静态语句块中的语句合并而成,编译器收集的顺序是由语句在源文件中的顺序所决定的,静态语句块只能访问到静态语句块之前的变量,定义在它之后的变量,在前面可以赋值,但是不能访问,例如如下的代码
public class TestStaticCode {
static {
i=2; //这里正常
System.out.println(i);//error: illegal forward reference
}
static int i =1;
}
<clinit>()
方法和类的构造方法(<init>()
方法)不同,不需要显式地调用父类的<clinit>()
方法,虚拟机会保证在调用子类的方法之前,父类的方法已经执行完毕。
<clinit>()
方法对于类或者接口来说不是必需的,如果一个类里面没有静态语句块,就不会有<clinit>()
生成。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法。
双亲委托模型
当类加载器加载一个类的时候,首先判断这个类是否已经被加载,如果没有加载则交给父加载器去加载。
hotspot
虚拟机中的一些类加载器(以 java8 为例)
-
启动类加载器(Bootstrap ClassLoader) 这个类加载器用来加载
$JAVA_HOME/lib
目录中,或者使用-Xbootclasspath
参数所指定的类库加载到虚拟机内存中。需要说明的是,这个类库必须是虚拟机识别的,例如rt.jar
,名字不符合要求的类库,即使放在这个文件夹中也不会被加载。这个加载器不能被 java 程序直接引用。 -
扩展类加载器(ExClassLoader) 这个用来加载
$JAVA_HOME/lib/ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,并且这个类加载器是可以被开发者直接使用的。 -
系统类加载器(AppClassLoader),又叫应用程序类加载器 负责加载用户类路径上所指定的类库,开发人员可以直接使用这个加载器,一般情况下这个是默认的类加载器。
-
用户自定义加载器 我们可以实现自己的类加载器,只需要继承 java.lang.ClassLoader 类,然后重写 findClass 方法,最后在 findClass 方法中直接调用 defineClass 方法就可以了,其他的 jdk 已近帮我们实现了。