04. 类加载机制


前言

Java 程序的运行过程为:

  • 我们自己写好 Java 程序之后会保存成 .java 文件,也就是说,.java 文件里面存储的是我们人能读懂的 Java 源代码,但是计算机并不认识。
  • 这时候,就需要 Java 编译器对 .java 文件进行编译,把源代码转换为二进制字节码,生成 .class 文件。
  • 计算机能够理解 .class 文件,但并不能直接运行,所以需要类加载机制进行加载,然后在 JVM 的内存中运行。


这篇文章我们主要介绍 Java 的类加载机制。


一、类的生命周期

一个 Java 类完整的生命周期会经历加载连接初始化使用卸载五个阶段,其中连接又包含验证、准备和解析三个部分。类的生命周期图示如下:

在这里插入图片描述

二、类加载过程

类生命周期的前三个阶段就是类加载过程(class loading),即加载连接初始化。类加载过程如下图:

在这里插入图片描述

1. 加载

加载是指将类的 .class 文件读入到内存,并为其创建一个 java.lang.Class 对象。也就是说,当程序中使用任何类时,系统都会为其建立一个 java.lang.Class 对象。

类的加载由类加载器完成,类的加载器通常由 JVM 提供,也称为系统类加载器。加载过程完成三个任务:

  • 通过类的完全限定名称获取定义该类的二进制字节流;
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构;
  • 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区中该类的各种数据的访问入口。

其中,类的二进制字节流由类加载器从不同的来源加载而来,主要有

  • 从本地文件系统加载 .class 文件;
  • 从 jar 包加载 .class 文件;
  • 通过网络加载 .class 文件,比如 Applet;
  • 把一个 Java 源文件动态编译,并执行加载。

2. 连接

连接是指把 Java 类的二进制码合并到 JVM 的运行状态的过程,即把类的二进制数据合并到 JRE(Java 运行时环境) 中。这个过程又包括三个部分:验证——>准备——>解析。

(1)验证: 确保 .calss 文件的字节流中包含的信息符合 JVM 规范,不会危害虚拟机自身安全(比如数组越界检查,除数非零检查等等)。

(2)准备: 为类的静态变量(注意,是静态变量,即类变量)分配内存,并设置默认初始值。

  • 准备阶段是给类的静态变量(static field)在方法区分配内存,并赋默认的初值(0 或 null)。如static int a = 10;,静态变量 a 在准备阶段被赋默认值 0,在初始化阶段才会把 10 赋给 a。
  • 而对于一般的成员变量,是在类实例化的时候,随对象一起分配在堆内存中。(注意:实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,类加载只进行一次,实例化可以进行多次)
  • 另外,静态常量(static final field)会在准备阶段就赋程序设定的初值,如static int a = 24;,静态常量 a 在准备阶段就被直接赋值为 24。

(3)解析: 将常量池的符号引用替换为直接引用的过程。

3. 初始化

在准备阶段,静态变量已经赋过一次系统要求的默认值,在初始化阶段,将根据程序员通过程序制定的主观计划去初始化静态变量和其它资源,即执行类构造器 <client>() 方法。 <client>() 是由编译器自动收集类中所有静态变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。

特别注意四点:

  • 由于编译器收集的顺序是由语句在源文件中出现的顺序决定的,所以静态语句块只能访问到定义在它之前的静态变量,定义在它之后的静态变量可以赋值,但不能访问。例如:
    public class Test {
    	static {
    		i = 0; // 给变量赋值可以正常编译通过
    		System.out.print(i); // 这句编译器会提示“非法向前引用”
    	}
    	static int i = 1;
    }
    
  • 初始化方法执行的顺序:虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。所以,第一个被执行类初始化方法的类一定是 java.lang.Object ,并且父类中定义的静态语句块的执行要优先于子类中定义的静态语句块。例如:
    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }
     
    static class Sub extends Parent {
        public static int B = A;
    }
     
    public static void main(String[] args) {
    	// 静态变量 B 的值是 2 而不是 1。
        System.out.println(Sub.B); // 2
    }
    
  • 接口中不可以使用静态语句块,但仍然有静态变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
  • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。

类初始化时机: JVM 规范中严格规定了有且只有五种情况必须对类进行初始化:

  • 使用 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先出发其初始化。生成这四条指令的情况:使用 new 创建类的实例的时候;读取或设置一个类的静态变量的时候;调用一个类的静态方法的时候。
  • 通过 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则必须先出发其初始化。
  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的类),虚拟机会先初始化这个主类。
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

三、类加载器

为了完成加载过程中的第一条(通过一个类的完全限定名称来获取该类的二进制字节流)功能,JVM 团队开发了一个模块——类加载器。为了给用户提供更好的拓展性,JVM 团队将这个过程的代码放到了 JVM 的外部,以便让开发人员可以自定义类加载器。

对于任意一个类,它的唯一决定方式是:类本身 + 加载该类的类加载器,因为每个类加载器都有自己独立的命名空间。所以说,两个类相等,不仅是两个类本身相等,还要使用同一个类加载器进行加载。

为了拓展性,JVM 提供了多种类加载器,用户也可以自行拓展。

  • 从 Java 虚拟机的角度,只有两种不同的类加载器:

    • 启动类加载器(Bootstrap ClassLoader):用 C++ 实现的,是虚拟机自身的一部分;
    • 所有其它的类加载器:用 Java 实现的,独立于虚拟机外部,都继承自抽象类 java.lang.ClassLoader
  • 从 Java 开发人员的角度,有三种不同类型的类加载器:

    • 启动类加载器(Bootstrap ClassLoader):负责加载 <\JAVA——HOME>\lib 目录中的,并且可以被虚拟机识别的文件(如 xx.jar);
    • 扩展类加载器(Extension ClassLoader):负责加载 <\JAVA_HOME>\lib\ext 目录中的所有类库,开发者可以直接使用扩展类加载器;
    • 应用程序类加载器(Application ClassLoader):它是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。

四、类加载机制

Java 的类加载机制主要有如下 3 种:

  • 全盘负责: 全盘负责是指,当一个类加载器负责加载某个类的时候,该类所依赖和引用的其它的类也由该类加载器负责加载,除非显式地使用另外一个类加载器来载入。
  • 双亲委派: 双亲委派是指,加载一个类时,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制: 缓存机制保证所有加载过的类会被缓存,当程序中需要使用某个类时,类加载器先从缓存中搜寻该类,如果缓存区不存在该类的对象时,系统才会去加载这个类。

双亲委派机制:

应用程序是由三种类加载器相互配合从而实现类加载,除此之外还可以加入自己定义的类加载器。双亲委派模型如下图:

在这里插入图片描述
双亲委派机制的工作原理:

如果一个类加载器收到了类加载请求,它首先把这个请求委托给它的父类的加载器去执行,如果父类加载器还有其父类加载器,则再向上委托,直至顶层的启动类加载器。如果父类加载器可以完成类加载任务,则成功返回;否则,子类加载器才尝试自己去加载。

双亲委派机制的好处:

双亲委派机制使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,从而使得基础类得到统一。通过这种层次关系,可以避免类的重复加载,当父类已经加载了该类时,子类加载器就没有必要再加载一次。比如,所有的类都继承自 Object 类,有了双亲委派机制,Object 类只会加载一次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值