Java虚拟机类加载机制(类加载过程、类加载器、双亲委派机制)

虚拟机类加载机制

在我们写好的代码编译生成xxx.class文件之后,是如何被JVM加载执行的呢?这就是虚拟机类加载机制要解决的问题了。

一、概述

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,就是虚拟机的类加载机制

Java支持动态扩展,与在编译时需要进行连接的语言不同,Java语言可以在运行时进行类的加载

二、类加载的时机

当一个类被加载到虚拟机内存中到卸载出内存为止,它的生命周期包括了七个阶段:加载、验证、准备、解析、初始化、使用、卸载

在这里插入图片描述

其中加载、验证、准备、初始化五个阶段的顺序是确定的,而解析阶段则并不确定(可以在初始化后执行)。

加载

虚拟机规范并没有强制约束什么时候进行加载,由虚拟机的实现者来自由把握。

加载阶段虚拟机需要完成三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象

在这里插入图片描述

注意:

虚拟机规范只是规定通过类的全限定名获取类的二进制字节流,并不一定要从xxx.class文件中获取,因此就有了经典的jar、war等方式

验证

验证是连接阶段的第一步,这一阶段的目的是确保加载进JVM的字节流符合JVM格式要求,不会危害虚拟机自身的安全。

因为虚拟机不要求代码必需由Java源码编译而来,甚至你可以使用十六进制编辑器写一个class文件,这样就有可能危害JVM(如Java语言虽然不支持goto语句,但是JVM指令有goto指令,完全可以在字节码层面篡改使用goto指令危害虚拟机)。所以JVM需要对加载的class文件进行验证。一般验证过程都会分为四个阶段:

  • 文件格式验证:验证字节流是否符合Class文件格式规范
    • 比如class文件魔数是否正确
    • 版本号是否在当前虚拟机处理范围
    • 常量池中是否有不被支持的常量类型(检查tag标志)
  • 元数据验证:验证字节码是否符合Java语言规范
    • 比如本类是否存在父类
    • 这个类的父类是否继承了不允许继承的类(被final修饰的类)
    • 如果不是抽象类,是否实现了父类或接口中的所有要求实现的方法
  • 字节码验证:主要进行数据流和控制流分析,对方法体进行校验分析
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,比如操作数栈中有一个int变量,那么指令使用时就不能用long类型或其他类型加载到本地变量表中
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体中的类型转换是有效的,比如检查多态可以父类引用指向子类实例,但是不能子类引用指向父类实例
  • 符号引用验证:主要对类自身以外(常量池中的各种符号引用)进行匹配性校验
    • 符号引用中通过字符串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述
    • 符号引用中的类、字段和方法是否可以被当前类访问

准备

经过加载和验证阶段之后,我们拿到Class文件就已经可以放心被加载了,所以这时候JVM正式为类变量分配内存设置类常量初始值的阶段(强调:类变量static修饰的变量),在JDK8中静态变量并不存储在方法区中,而是存储在堆内存中。但static变量与static final变量的初始值设置时机仍有区别。

  • static变量在准备阶段赋零值,在初始化阶段赋初始值
  • final变量在准备阶段赋初始值
    下面详细解释:
public static int value = 123;
public static final int value = 123;

以上两个代码的初始化时机就不同,来看反编译后的文件,首先看static变量
在这里插入图片描述

可以看到在内存分配时候并没有进行值的初始化,看下图发现static变量初始化是在初始化阶段执行clinit方法时进行的

在这里插入图片描述

现在看final类型变量反编译后的文件,可以看出有ConstantValue属性在准备阶段就进行了值的初始化(此情况只适用于常量,如果final类型是个引用类型则仍需要等到初始化阶段的clinit方法进行初始化)

在这里插入图片描述

解析

将常量池中的符号引用转换为直接引用的过程。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以无歧义地地位到目标即可。符号引用的引用目标并不一定被加载到内存中

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用于虚拟机实现的内存布局相关。

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。

初始化

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是执行<clinit>()方法的过程。

<clinit>方法:是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问

<clinit>方法与<init>方法不同,它不需要显式调用父类构造器,虚拟机会保证父类的<clinit>方法执行完毕后执行本类的<clinit>方法。因此虚拟机执行的第一个<clinit>方法肯定是Object类的

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类都会生成<clinit>方法,但是执行接口的<clinit>方法不会触发父接口的<clinit>方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法

虚拟机会保证一个类的<clinit>方法在多线程环境下被正确的加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程会执行,其余的线程都需要进入阻塞等待

其实也并不难理解,例:

加载将xxx.class文件加载到虚拟机内存中,之后验证xxx.class的正确性,如果验证成功就需要准备为类变量分配内存,之后才可以去解析和初始化,但是由于Java支持动态扩展(比如多态,只有在运行期间才知道具体使用的类)

三、类加载器

从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstarp ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分。还有就是其他的类加载器,这些类加载器由Java语言实现,独立与虚拟机,并且全部继承自java.lang.ClassLoader

从开发人员角度类加载器还可以细分为:

  • 启动类加载器:负责加载JAVA_HOME/jre/lib目录下的类(常用的工具类都在lib/rt.jar里)
  • 扩展类加载器:负责加载JAVA_HOME/jre/lib/ext目录下的类
  • 应用类加载器:负责加载classpath下的类(即我们自己项目中写的类)
  • 用户自定义加载器:一般不用

双亲委派模型

在这里插入图片描述

双亲委派模型要求除了启动类加载器之外,其余的所有类加载器都应当有自己的父类加载器,类加载器之间关系如图。

双亲委派机制工作流程:

当一个类加载器收到了类加载的请求时,他首先不会自己尝试加载这个类而是先交给父类加载器去完成,每层的类加载器都是如此,最终都传送到顶层的类加载器中,只有父类加载器反馈自己无法完成加载请求时子类加载器才会尝试进行加载

在这里插入图片描述

优点:使Java类之间具备了一种带有优先级的层次关系,例如Object类总会被同一个类加载器加载。保证所有类使用的同时同一个Object类。否则由不同类加载器加载出许多不同的Object类,那该有多么混乱。而且使用双亲委派机制可以保证如果我们写出与JDK中工具类同名的类无法被运行,保证了我们的代码不会污染JAVA的环境

看下双亲委派机制的源码实现java.lang.ClassLoader.loadclass():

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 {
                    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;
        }
    }

由源码可以看出类加载是线程安全的,先检查类是否已经被加载过,如果没有进入if判断是否有父类加载器,如果有递归调用父类加载器,如果没有则调用启动类加载器进行加载,如果父类无法加载则进入下方的if(c==null)使用自己的类加载器加载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值