《深入理解Java虚拟机》:类加载和初始化(二)
在去年看《深入理解Java虚拟机》的时候,写过一篇关于类加载和初始化的博客,最近又在看这一块的知识,发现还是有很多东西没有理解好。借助于网上的一些博客,学习了一点新的知识,并整理如下。
1、类加载过程
类加载过程由如下几个阶段构成:装载、链接、初始化。其中链接包括:验证、准备和解析三个阶段。
1、装载:查找并加载类的二进制数据
2、链接:
验证:确保被加载类的正确性
准备:为类的静态属性分配内存并初始化为默认值。
解析:把类中的符号引用转换为直接引用
3、初始化:为类的静态变量赋予正确的初始值。
那为什么还要有验证这一步骤呢?
首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。
准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的.
如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
2.类的初始化
类什么时候才被初始化:
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类),注:子类初始化问题:满足主动调用,即父类访问子类中的静态变量、方法,子类才会初始化;否则仅父类初始化。具体看这篇博客:http://blog.csdn.net/u010412719/article/details/47059439
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
只有这6中情况才会导致类的类的初始化。
类的初始化步骤
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
3.加载器
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:(七牛云存储好像出bug了,访问不了,因此就没有图片)
Bootstrap classLoader:加载核心库文件
Extension classLoader:加载扩展库文件
App classLoader:加载classpath路径下的jar包,我们写的java类,一般都是由它加载,除非你自己制定个人的类加载器。
loadClass方法的源码如下:
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
/* 如果有父类加载器,则交给父类加载器去加载。 如果没有父类加载器,则说明此加载器为extension classLoader,直接交给Bootstrap classLoader加载器去加载。 */
if (parent != null) {
c = parent.loadClass(name, false);//交给父类来加载
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
/* 如果父类加载器没有加载成功,则自己加载。 */
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
源码思想思路如下:
假设加载类A,加载过程如下
1、首先检查当前加载器是否加载了类 A,如果没有,则进行 2,否则进行 7
2、检查当前加载器是否有父加载器,如果有,则进行 3, 否则说明当前加载器为extension classLoader,因此进行 4.
3、当类A委托给父加载器进行加载(父类加载器加载的过程和这个过程类似,也是先检查类A是否已经加载,
如果没有,检查是否有父类加载器,如果有,则进一步委托给父加载器),如果加载成功,则 进行 7,否则进行 5.
4、将类A委托给Bootstrap classLoader进行加载,如果加载成功,则进行 7,否则进行 5.
5、检查类A是否已经加载成功,如果没有,则进行 6,否则进行 7.
6、当前加载前将自己亲自加载类 A .
7、退出
总结:加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类,即从Bootstrap classLoader到custom classLoader逐层尝试加载此类。
类的卸载
参考博客