Java源代码被编译成class字节码,最终需要加载到虚拟机中才能运行。整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
加载
双亲委派模型工作过程:当一个类加载器收到类加载任务,优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
双亲委派模型有什么好处?
比如位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类。
验证
为了确保Class文件符合当前虚拟机要求,需要对其字节流数据进行验证,主要包括格式验证、元数据验证、字节码验证和符号引用验证。
准备
在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。
private static int var = 100;
准备阶段完成后,var值为0,不是100。在初始化阶段,才会把100赋值给val,但是有个特殊情况:
private static final int VAL = 100;
在编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。
解析
解析阶段是将常量池中的符号引用替换为直接引用的过程,符号引用和直接引用有什么不同?
1、符号引用使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中。
2、直接引用可以是直接指向目标的指针、相对偏移量或则能间接定位到目标的句柄。
初始化
初始化阶段是执行类构造器
private static int value = 100;
static int a = 100;
static int b = 100;
static int c;
static {
c = a + b;
System.out.println("it only run once");
}
1.
2.
3.为了防止多次执行
4.注意:执行接口的
类初始化场景
虚拟机中严格规定了有且只有5中情况必须对类进行初始化。
- 执行new、getstatic、putstatic、invokestatic指令;
- 使用reflect对类进行反射调用;
- 初始化一个类的时候,父类还没有初始化,会事先初始化父类;
- 启动虚拟机时,需要初始化包含main方法的类;
- 在JDK1.7中,如果java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化。
以下几种情况,不会触发类初始化
1.通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {
static {
System.out.println("child init!");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Child.a);
}
}
输出结果为:
parent init!
100
2.定义对象数组,不会触发该类的初始化。
public class Init{
public static void main(String[] args){
Parent[] parents = new Parent[10];
}
}
无输出,说明没有触发类Parent的初始化。
3.常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类初始化。
class Const {
static final int A = 100;
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Const.A);
}
}
输出:
100
说明没有触发类Const的初始化,在编译阶段,Const类中常量A的值100存储到Init类的常量池中,这两个类在编译成class文件后就没有联系了。
4.通过类名获取Class对象,不会触发类的初始化。
public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class c_dog = Dog.class;
Class clazz = Class.forName("zzzzzz.Cat");
}
}
class Cat {
private String name;
private int age;
static {
System.out.println("Cat is load");
}
}
class Dog {
private String name;
private int age;
static {
System.out.println("Dog is load");
}
}
执行结果:Cat is load,所以通过Dog.class并不会触发Dog类的初始化动作。
5.通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6.通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
new ClassLoader(){}.loadClass("zzzzzz.Cat");
摘自占小狼JVM类加载的那些事