JVM提供了以下3种系统的类加载器:
启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将
扩展类加载器(Extension ClassLoader):扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载
应用程序类加载器(Application ClassLoader):也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
除此以外,还可以通过继承实现自定义类加载器, 他们的关系如下:
双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。
双亲委派模型的工作过程:
如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。
只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。
双亲委派模型的代码实现:
双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中,主要逻辑为:
主要源代码如下:
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先检查类是否被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2 没有则调用父类加载器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父类加载失败,抛出ClassNotFoundException 异常后
c = findClass(name);
}
}
if (resolve) {
//5 再调用自己的findClass() 方法。
resolveClass(c);
}
return c;
}
了解了类加载器, 接下来再看看类加载机制~
先看一道经典的面试题:
public class TestA{
private static TestA mInstance = new TestA();
public static int value1;
public static int value2 = 0;
private TestA(){
value1++;
value2++;
}
public static TestA getInstance(){
return mInstance;
}
}
public class TestB{
public static int value1;
public static int value2 = 0;
private static TestB mInstance = new TestB();
private TestB(){
value1++;
value2++;
}
public static TestB getInstance2(){
return mInstance;
}
}
//请问执行的输出结果?
public static void main(String[] args) {
TestA testA = TestA.getInstance();
System.out.println("TestA value1 = " + testA.value1);
System.out.println("TestA value2 = " + testA.value2);
TestB testB = TestB.getInstance2();
System.out.println("testB.value1 = " + testB.value1);
System.out.println("testB.value2 = " + testB.value2);
}
请说出运行结果。
我们先跑一下代码,运行看看结果:
JVM类加载分为五步:加载、链接、初始化、使用、卸载, 其中链接又细分为:验证、准备、解析三个步骤, 以下是流程图:
下面解释类加载的各个步骤的内容。
加载主要是将.class文件(或者是网络中获取的zip包)中的二进制字节流读入到JVM中, 这个过程主要包含三部分:
链接主要分为三步:验证、准备、解析。
验证主要是确保加载近来的字节流符合JVM规范。
验证的主要过程有:
验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。
准备主要是为静态变量在方法区分配内存,并设置初试默认值,比如int默认为0,boolean默认为false, 注意,这里只是初始化默认值,还并没有赋值初始值, 比如 static int a = 10, 这一句在这里只是执行了给a分配内存空间并默认为0。在初始化阶段才赋值。
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化是类使用前的最后一步,主要是根据代码中的赋值语句为类变量赋值的过程。需要注意的是:当有父类且父类未初始化时,先去初始化父类, 再进行子类的初始化。
类初始化的时机:
以上情况统称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。
总结一下,当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载,连接,初始化三个步骤来对该类进行初始化,JVM将会连续完成这三个步骤,也把这三个步骤统称为类加载或类初始化类,然后就可以实用类了。
我们回过头看一下刚刚的面试题:
前面两行和类TestA相关的打印是: 1 0
原因分析:
1.首先执行 TestA testA = TestA.getInstance();调用了类的静态方法,触发类的加载
2.类的验证
3.准备:为静态变量分配内存,设置默认值。这里为mInstance(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0
4.类的解析
5.类初始化化,按代码顺序依次为类的静态变量赋值和执行静态代码快。依次执行代码:
private static TestA mInstance = new TestA();
public static int value1;
public static int value2 = 0;
所以 先执行构造方法 ,value1, value2的值都变为1, 然后value1没有任何操作,value2又被赋值为0, 所以输出结果为 1, 0;
后面两行的打印是 1 1:
原因分析:
1.首先执行 TestB testB = TestB.getInstance();调用了类的静态方法,触发类的加载
2.类的验证
3.准备:为静态变量分配内存,设置默认值。这里为value1,value2(基本数据类型)设置默认值0 ,mInstance(引用类型)设置为null,
4.类的解析
5.类初始化化,按代码顺序依次为类的静态变量赋值和执行静态代码快。依次执行代码:
public static int value1;
public static int value2 = 0;
private static TestB mInstance = new TestB();
所以 先执行前面两行,value1默认为0,, value2的值为0, 执行构造方法,都+1,, 所以输出结果为 1, 1;