java虚拟机JVM--java类加载器和类加载机制

java类加载器

JVM提供了以下3种系统的类加载器:

启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)

扩展类加载器(Extension ClassLoader):扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

应用程序类加载器(Application ClassLoader):也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

除此以外,还可以通过继承实现自定义类加载器, 他们的关系如下:

在这里插入图片描述

理解双亲委派模式

双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。

双亲委派模型的工作过程:

如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。

只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。

双亲委派模型的代码实现:

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中,主要逻辑为:

  • 1、首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;
  • 2、若父类加载器为空,则默认使用启动类加载器作为父加载器;
  • 3、若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

主要源代码如下:

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类加载分为五步:加载、链接、初始化、使用、卸载, 其中链接又细分为:验证、准备、解析三个步骤, 以下是流程图:

在这里插入图片描述

下面解释类加载的各个步骤的内容。

1.加载

加载主要是将.class文件(或者是网络中获取的zip包)中的二进制字节流读入到JVM中, 这个过程主要包含三部分:

  • 1.通过类的全限定名获取该类的二进制字节流;
  • 2.将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 3.在内存中生成一个该类的java.lang.Class对象, 作为这个类得各种数据的访问入口
2.链接

链接主要分为三步:验证、准备、解析。

2.1验证

验证主要是确保加载近来的字节流符合JVM规范。
验证的主要过程有:

  • 1.文件格式验证
  • 2.元数据验证:是否符合java语义规范,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法。。。等等。。。
  • 3.字节码验证:确定程序语法合法,符合逻辑
  • 4.符号引用验证:确保下一步解析能正常执行

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

2.2准备

准备主要是为静态变量在方法区分配内存,并设置初试默认值,比如int默认为0,boolean默认为false, 注意,这里只是初始化默认值,还并没有赋值初始值, 比如 static int a = 10, 这一句在这里只是执行了给a分配内存空间并默认为0。在初始化阶段才赋值。

2.3解析

解析是虚拟机将常量池的符号引用替换为直接引用的过程。

3.初始化

初始化是类使用前的最后一步,主要是根据代码中的赋值语句为类变量赋值的过程。需要注意的是:当有父类且父类未初始化时,先去初始化父类, 再进行子类的初始化。

类初始化的时机:

  • 1.使用new创建类对象时
  • 2.读取或者设置类的静态字段时
  • 3.调用类静态方法时
  • 4.使用反射Class.forName()对类进行反射调用时,
  • 5.初始化一个类是, 有父类则先初始化父类,不过接口除外,父接口只有被调用时才会被初始化, 直接使用子类调用父类的静态字段, 只会初始化父类
  • 6.被标明为启动类的类(即包含main方法的类)
  • 7.当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况统称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。

二、面试题分析

总结一下,当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载,连接,初始化三个步骤来对该类进行初始化,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;

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值