(四)Java 虚拟机是如何加载Java类的?

类的加载过程

类的整个加载过程从类字节流通过虚拟机的类加载器加载到内存供虚拟机使用,到垃圾收集器回收,其生命周期可分为加载、链接、初始化、使用、卸载。其中链接可分为验证、解析、准备,如下:

  • 加载

加载是虚拟机借助类加载器查找字节流并且创建类的过程,从设计图(Class 结构文件)到产品实物(类)的过程。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。

  类加载器

类加载器好比工程师,负责产品从设计图到加工生产实物产品的过程。一个工厂(虚拟机)中,工程师(类加载器)有内部既有(内置)的,也可以继续外聘(自定义)。众多工程师中,对于不同级别,负责不同的工作任务。虚拟机中内置的类加载器也同样如此:负责加载 JAVA_HOME/jre/lib/ 中的类的加载器叫 Bootstrap ClassLoader ; 负责加载 JAVA_HOME/jre/lib/ext/ 中的类的加载器叫 Extension ClassLoader ; 负载加载项目 class path 中的类的加载器叫 Application ClassLoader ; 开发人员可以自定义类加载器,此处暂且叫它们 User ClassLoader  :

public static void main(String[] args) {

    System.out.println(System.getProperty("sun.boot.class.path"));
    System.out.println("\n");
    System.out.println(System.getProperty("java.ext.dirs"));
}

// Bootstrap ClassLoader 加载目录:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes

 

// Extension ClassLoader 加载目录:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext

 

类加载器搜索加载类时候,它在尝试亲自搜索某个类之前,先把这个任务委派给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器 Bootstrap ClassLoader 尝试加载,如果没加载到,则把任务委派给 Extension ClassLoader 尝试加载,如果也没加载到,则委派给 Application ClassLoader 尝试进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

双亲委派机制

类加载器使用的是双亲委托机制来搜索加载类的,每个类加载器实例都有一个父类加载器的引用(组合),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它类加载器实例的的父类加载器。

当一个类加载器实例需要加载某个类时:

  1. 判断该类是否已经加载,如果已经加载,直接返回实例,否则继续执行
  2. 判断父类加载是否存在,如果存在,加载任务委派给父类加载器,否则继续执行
  3. Boostrap ClassLoader 加载该类,如果加载成功,直接返回实例,否则继续执行
  4. 执行 findClass 方法加载该类,如果都没加载到该类,着抛出 ClassNotFoundException 
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;
        }
    }

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

 

  • 链接

链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

 

验证阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件。

准备阶段的目的,则是为被加载类的静态字段分配内存。除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

 

  • 初始化

在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

 

P.S. 本系列文章为学习出自郑雨迪的《深入拆解 Java 虚拟机》课程整理笔记。购买请扫描下方二维码: 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值