虚拟机类加载机制

虚拟机类加载机制

1 前言

类从被加载到虚拟机内存中开始,到卸载出内存结束。其整个生命周期包括:

  • 加载 Loading
  • 验证 Verification
  • 准备 Preparation
  • 解析 Resolution
  • 初始化 Initialization
  • 使用 Using
  • 卸载 Unloading

其中验证,准备,解析三个阶段统称为连接Linking
在这里插入图片描述

2 类的加载时机

虚拟机规范严格规定了,只有在主动使用才必须立即对类进行初始化。除此之外,所有引用类的方法被称为被动引用,都不会触发初始化。

  1. 遇到助记符new (实例化对象),getstatic (读取静态字段),putstatic (设置静态字段),invokestatic (调用静态方法)这4个字节码指令时,如果类没有进行初始化,则先对其初始化。
  2. 使用java.lang.reflect包的方法对类进行反射调用。
  3. 子类初始化时,先触发父类的初始化。
  4. 虚拟机启动时指定要执行的主类(main方法的类)时。
  5. java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄所对应的类。

3 类加载的过程

3.1 加载

加载是类加载(Class Loading)的一个阶段。在加载阶段需要完成:

  1. 将类的二进制字节流读取到内存中。
  2. 将字节流转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表该类的java.lang.Class对象,用于封装方法区中的数据结构。
3.2 验证

这一阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。大致分为4个验证动作:文件格式校验,元数据验证,字节码验证,符号引用验证。

3.2.1 文件格式验证

检验字节流是否符合Class文件格式的规范,这阶段的验证是基于二进制字节流进行的,只用通过验证后,字节流才会进去内存的方法区中进行存储,后面3个验证全部是基于方法区的存储结构进行的。

3.2.2 元数据验证

对字节码描述的信息进行语义分析,以保证符合java语言规范的要求。

3.2.3 字节码验证

通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。

3.2.4 符号引用验证

该验证阶段发生在虚拟机将符号引用转换为直接引用的时候。主要验证全限定名是否能找到该类,在指定类中是否存在所描述的方法和字段,符合引用中的类,字段,方法的访问性。此阶段的验证是确保解析动作能正常执行,如果无法通过会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError等。

3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值,这些变量使用的内存都在方法区中进行分配。例如,public static int value = 123;在准备阶段后的值为0,因为这时还没有开始执行任何java方法。把value赋值为123的动作在初始化阶段才会执行。

3.4 解析

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

  • 符号引用:以符号来描述所引用的目标,符号可以是任何形式的字面量。
  • 直接引用:直接指向目标的指针,相对偏移量或是一个能简接定位到目标的句柄。

解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用。

3.5 初始化

真正开始执行类中定义的java程序(字节码)。 类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化。

4 类加载器

4.1.1 启动类(Bootstrap)加载器:

它是由C++实现的本地方法,不属于Java类范畴,是虚拟机自身的一部分,不能够被直接引用,主要被用于加载java所需的核心jar包,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,属于顶级类加载器。

4.1.2 扩展类(Extension)加载器:

它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。

4.1.3 应用程序类(Application)加载器:

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

5 双亲委派模型 (Parents Delegation Model)

下图展示的类加载器之间的这种层次关系,称为双亲委派模型。除了顶层的启动类加载器之外,其余的类加载器都有自己的父类加载器。类加载器之间的父子关系是使用组合(Composition)关系来复用父加载器的代码。
在这里插入图片描述

双亲委派工作过程:如果一个类加载器收到了类加载的请求,它会把这个请求委派给父类加载器loadClass()去完成,以此类推。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。如果父类加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法加载。

双亲委派的优点:即便用户实现了一个与rt.jar类库中已有类重名的java类,也不会被加载运行,这样保证了java类型体系中最基础的行为,避免了程序的混乱。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
        // 同步上锁
        synchronized (getClassLoadingLock(name)) {
            // 先查看这个类是不是已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 父加载器为空则默认使用启动类加载器作为父加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 父类加载器加载失败时再调用本身的findClass来进行类加载
                }

                if (c == null) {
                    // 如果还是没有获得该类,调用findClass找到类
                    c = findClass(name);
                }
            }
            // 连接类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

6 破坏双亲委派模型

双亲委派很好的解决了各类加载器的基础类的统一问题,基础类总是作为被用户调用的API,但如果基础类要调用回用户的代码,这时启动类加载器还没完成用户代码的加载。 以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。

为了解决这个问题,引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()进行设置。有了上下文类加载器,父类加载器就可以请求子类加载器去完成类加载的动作,这种行为实际上打通了双亲委派模型的层次结构来逆向使用类加载器。Java中所有涉及SPI的加载动作都采用这种方式,如JNDI,JDBC,JCE,JAXB,JBI等。

另一种破坏双亲委派的场景是用户对程序动态性的追求,如代码热替换(HotSwap),模块热部署(Hot Deployment)。其中OSGI实现模块化热部署的关键是它自定义的类加载器机制的实现。每个程序模块(OSGi 中称为Bundle)都有一个自己类的加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉已实现代码的热替换,此时类加载器不再是双亲委派中的树状结构,而是网状结构。

7 参考部分

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值