虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制(《深入理解java虚拟机》)
类从被加载到虚拟机内存中开始,到卸载出内存位置,它的整个生命周期包括:加载(Loading)、验证(Verfictaion)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图
1、加载
加载是类加载过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:
1)通过一个类的全新定名类获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2、验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不危害虚拟机的安全(具体细节这里就不说了,其实我也不清楚,哈哈,大家简单了解一下即可,没有必要太深入)
3、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随对象一起分配在java堆中。
4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用。
5、初始化(这个地方对我们用户来说比较重要,需要详细看一下)
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
类初始化阶段是类加载过程的最好一步,并且类初始化只加载一次,说到类初始化,不得不提的就是主动引用和被动引用。
1)主动引用
虚拟机规范是严格规定了有且只有5中情况必须立刻对类进行初始化
(1)遇到new(使用new 关键字实例化一个对象)、getstatic(读取一个类的静态字段)、putstatic或者invokestatic(设置一个类的静态字段)这4条指令的时候,如果类没有进行过初始化。则需要先触发其初始化。(我们可以简单理解为创建类的实例时,访问类的静态变量或者对该静态变量赋值时)
(2)使用反射进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化
(4)程序启动需要触发main方法的时候,虚拟机会先触发这个类的初始化
(5)当使用jdk1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic、REF_pusStatic、REF_invokeStatic的方法句柄(句柄中包含了对象的实例数据和类型数据,句柄是访问对象的一种方法。句柄存储在堆中),并且句柄对应的类没有被初始化,那么需要先触发这个类的初始化。
2)被动引用
5种之外情况就是被动引用。被动引用的经典例子有:
(1)通过子类引用父类的静态字段
这种情况不会导致子类的初始化,因为对于静态字段,只有直接定义静态字段的类才会被触发初始化,子类不是定义这个静态字段的类,自然不能被实例化。
(2)通过数组定义来引用类,不会触发该类的初始化。例如, Clazz[] arr = new Clazz[10];并不会触发。
(3)常量不会触发定义常量的类的初始化。因为常量在编译阶段会存入调用常量的类的常量池中,本质上并没有引用定义这个常量的类,
所以不会触发定义这个常量的类的初始化。
初始化这个地方有一个例子必须要讲一下
package com.slowly.reference;
class Singleton{
private static Singleton singleton = new Singleton();
public static int count01;
public static int count02=0;
Singleton(){
count01++;
count02++;
}
public static Singleton getInstance(){
return singleton;
}
}
public class Test {
public static void main(String[] args){
Singleton singleton = Singleton.getInstance();
System.out.println("count01的值为:"+singleton.count01);
System.out.println("count02的值为:"+singleton.count02);
}
}
看看这个例子,大家先不要看下面的答案,可以猜一下输出结果是什么?
大家是不是以为结果会是
count01的值为:1
count02的值为:1
呢,其实是不对的,且看下面的正确答案
输出结果为:
count01的值为:1
count02的值为:0
为什么会这样呢?其实这就是虚拟机的类加载的过程导致的
在此过程中,先会对类进行加载,包括变量,但并不会初始化,按照上面的例子,第一步加载Singleton,然后静态变量count01和count02为基本数据类型,默认值为0;然后
获取实例时getInstance,构造函数分别对count01和count02加一,此时两个变量的值都为1,然后会调用初始化
public static int count02=0;
此时count02被初始化为0。所以最后输出为
count01的值为:1
count02的值为:0
那么问题又来了如果程序改成这样呢,
package com.slowly.reference;
class Singleton{
public static int count01;
public static int count02=0;
private static Singleton singleton = new Singleton();
Singleton(){
count01++;
count02++;
}
public static Singleton getInstance(){
return singleton;
}
}
public class Test {
public static void main(String[] args){
Singleton singleton = Singleton.getInstance();
System.out.println("count01的值为:"+singleton.count01);
System.out.println("count02的值为:"+singleton.count02);
}
}
相信大家看了前面的介绍,一定能够得到答案,这里就不给答案了
下面就是对类加载的双亲委派模型的介绍,和自定义类加载器将在下一篇博文中给出手把手教你写自定义类加载器