JVM类加载机制分为5个部分:加载、验证、准备、解析、初始化。
一、加载
加载是类加载过程的第一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的的各种数据的入口。注意这里并非一定要从class文件获取,这里既可以从zip包文件读取(比如从jar包和war包读取),也可以在运行时计算生成(动态代理),也可以有其他文件生成(比如有jsp文件转换成对应的class文件)。
二、验证
这一阶段的主要目的是为了确保class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害当前虚拟机的自身安全。
三、准备
准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,即在方法区中分配这些变量需要的内存空间。注意这里所说的初始值概念,比如:
public static int v = 8000;
实际上变量v在准备阶段过后的初始值为0而不是8000,将v复制8000的putstatic指令是程序被编译后,存放在类构造器<client>方法之中,这个在后面解释。但是,如果声明为:
public static final int v = 8000;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v的值设为8000。
四、解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_Info
CONSTANT_Method_Info
等类型的常量。
下面来看看符号引用和直接引用的概念:
符号引用与虚拟机实现的布局无关,引用的目标不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中。
直接引用是可以指向目标的指针,相对偏移量,或是一个可以间接定位到目标的句柄。如果有了直接引用,那么引用的目标必然已经存在于内存之中。
五、初始化
初始化阶段是类加载的最后一个阶段。前面的类加载阶段之后,出了在加载阶段可以自定义类加载器之外,其他操作都有jvm主导。到了初始化阶段,才开始真正执行类中定义的java程序代码。
初始化阶段是执行类构造器<Client>方法的过程。<Client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。ps:如果一个类中没有对静态变量的赋值也没有静态语句块,那么编译器可以不为这个类生成<client>方法。
注意以下几种情况不会执行类的初始化:
1.通过子类引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化。
2.定义对象数组,不会触发该类的初始化。
3.常量在编译期间会存入调用类的常量池中,本质上并没有引用定义常量的类,不会触发初始化常量所在的类。
4.通过类名获取Class对象,不会触发类的初始化。
5.通过Class.forName加载指定的类时,如果指定参数initialize为false时,也不会触发类的初始化,其实这个参数是告诉虚拟机,是否要初始化这个类。
6.通过ClassLoader的默认loadClass方法也不会触发类的初始化。
类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了三种类加载器:
启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib目录中的,或通过Xbootclasspath参数指定路径中的,且被虚拟机认可的类(按文件名识别,如 rt.jar)。
扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量路径指定的类库。
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
JVM使用双亲委派模型进行类的加载,当然我们也可以通过集成java.lang.ClassLoader实现自定义类加载器。
当一个类加载器收到类加载任务,会先交给父类加载器去完成,因此最终加载任务会传递到顶层的启动类加载器,只有当父类无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是,比如加载位于rt.jar包中的java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器加载最终得到的都是同一个Object对象。
在有些情境中可能会出现要我们自己来实现一个类加载器的需求,由于这里涉及的内容比较广泛,我想以后单独写一篇文章来讲述,不过这里我们还是稍微来看一下。我们直接看一下jdk中的ClassLoader的源码实现:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
1.首先通过 Class c = findLoadedClass(name);判断一个类是否已经被加载过。
2.如果没有被加载过,执行 if(c == null)中的语句,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父加载器是启动类加载器(Bootstrap ClassLoader)。
3.最后根据resolve的值,判断这个class是否需要解析。