Java类加载机制

请直接看原文

原文链接:Java类加载机制 - 知乎 (zhihu.com)


简单总结类加载的5个步骤:

1.类加载:将类的class文件加载到方法区

2.验证:验证class文件的正确性

3.准备:这个阶段,类的静态字段信息(即使用 static 修饰过的变量)会得到内存分配,并且设置为初始值。

4.解析:这个阶段,虚拟机会把这个Class文件中类常量池的字面量和符号引用,加载到运行时常量池,然后字面量不做变化,只是将符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。

5.初始化:类中static修饰的变量会赋予程序员实际定义的"值",同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

下面看详细介绍:


简介

在Java的世界里,每一个类或者接口,在经历编译器后,都会生成一个个.class文件。

类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。

类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。

除了使用卸载 两个过程,前面的5 个阶段 加载、验证、准备、解析、初始化 的执行过程**,**就是类的加载过程。

类加载的时机

大多数人在问 "类什么时候加载" 和 "类什么时候初始化" ,从语境上来说,都是在问同一个问题,就是这个.class文件什么时候被读取到虚拟机的内存中,并且达到可用的状态。

但严格意义上来说,加载和初始化,是类生命周期的两个阶段。

对于什么时候加载,Java虚拟机规范中并没有约束,各个虚拟机都可以按自身需要来自由实现。但绝大多数情况下,都遵循"什么时候初始化"来进行加载。

什么时候初始化?Java虚拟机规范有明确规定,当符合以下条件时(包括但不限于),虚拟机内存中没有找到对应类型信息,则必须对类进行"初始化"操作:

  • 使用new实例化对象时、读取或者设置一个类的静态字段或方法时
  • 反射调用时,例如 Class.forName("com.xxx.MyTest")
  • 初始化一个类的子类,会首先初始化子类的父类
  • Java虚拟机启动时标明的启动类
  • JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化

初始化阶段开始之前,自然还是要先经历 加载、验证、准备 、解析的。

类的加载过程

从简介中,我们知道,类的加载过程分 5 个阶段,其中 验证、准备、解析 可以归纳为 "连接" 阶段。

需要注意的是,这5个阶段,并不是严格意义上的按顺序完成,在类加载的过程中,这些阶段会互相混合,交叉运行,最终完成类的加载和初始化。

例如在加载阶段,需要使用验证的能力去校验字节码正确性。在解析阶段,也要使用验证的能力去校验符号引用的正确性。或者加载阶段生成Class对象的时候,需要解析阶段符号引用转直接引用的能力等等......

接下来,我们详细分解一下,这5个阶段,都做了什么事情。

1.加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名去找到其对应的.class文件
  2. 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

Java虚拟机并没有规定类的字节流必从.class文件中加载,在加载阶段,程序员可以通过自定义的类加载器,自行定义读取的地方,例如通过网络、数据库等。

2.验证

Class文件中的内容是字节码,这些内容可以由任何途径产出,验证阶段的目的是保证文件内容里的字节流符合Java虚拟机规范,且这些内容信息运行后不会危害虚拟机自身的安全。

验证阶段会完成以下校验:

文件格式验证:验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等

元数据验证:对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等

字节码验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。

符号引用验证 :发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段不是必须的,虽然这个阶段非常重要。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。

3.准备

这个阶段,类的静态字段信息(即使用 static 修饰过的变量)会得到内存分配,并且设置为初始值。

对于该阶段有以下几个知识点需要注意:

1、内存分配仅包括 static 修饰过的变量,而不包括实例变量,实例变量得等到对象实例化时分配内存。

2、初始值指的是变量数据类型的默认值,而不是被在Java代码中被显式地赋予的值。但是,当字段信息被 final 修饰成常量(ConstantValue)时,这个初始值就是Java代码中显式地赋予的值。

例如:public static int value = 3
类变量 value 在准备阶段设置的初始值 是 0 ,不是 3。把value赋值为3的 putstatic 指令是在程序编译后,存放于类构造器 <clinit>() 方法中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
当使用 final 修饰后:public static final int value = 3
类变量 value 在准备阶段设置的初始值 是 3,不是 0。

3、在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。

4.解析

这个阶段,虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行"连接"的过程

什么是符号引用?
Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标。

什么是直接引用?
直接引用就是可以直接或间接指向目标内存位置的指针或句柄。

引用的类型,还未加载初始化怎么办?
当出现这种情况,会触发这个引用对应类型的加载和初始化。

5.初始化

这是类加载的最后一个步骤啦,初始化的过程,就是执行类构造器 <clinit>()方法的过程。

当初始化完成之后,类中static修饰的变量会赋予程序员实际定义的"值",同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

<clinit>() 方法的作用是什么?

还记得么?在准备阶段,已经对类中static修饰的变量赋予了初始值。<clinit>() 方法的作用,就是给这些变量赋予程序员实际定义的"值"。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

<clinit>() 方法是什么?

<clinit>() 方法 和 <init> 方法是不同的,它们一个是"类构造器",一个是实例构造器。
Java虚拟机会保证子类<clinit>() 方法在执行前,父类的 <clinit>() 已经执行完毕。而 <init> 方法则需要显性的调用父类的构造器。
<clinit>() 方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成<clinit>()方法。

额外扩展

当一个符合Java虚拟机规范的字节流文件,经历 加载、验证、准备、解析、初始化这些阶段相互协作执行完成之后,加载阶段读取到的Class字节流信息,会按虚拟机规定的格式,在方法区保存一份,然后Java 堆中,会创建一个 java.lang.Class 类的对象,这个对象描述了这个类所有信息,也提供了这个类在方法区的访问入口。

方法区中,使用同一加载器的情况下,每个类只会有一份Class字节流信息
Java堆中,使用同一加载器的情况下,每个类只会有一份 java.lang.Class 类的对象

类加载器

还记得在加载 阶段,通过类的全限定名,获取该类字节流数据的这个动作么,类加载器就是用来实现这个动作的。

当年为了满足浏览器上 Java Applet 的需求,Java的开发团队设计了类加载器,它独立于Java虚拟机外部,允许程序员按自身需要自行实现类加载器。这是一项非常优秀的创新,它让同一个类可以实现访问隔离、OSGi、程序热部署等等。发展至今,类加载器已经是Java技术体系的一块重要基石。

三层类加载器介绍

**启动类加载器(Bootstrap Class Loader):**负责加载<JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数制定的路径,例如 jre/lib/rt.jar 里所有的class文件。由C++实现,不是ClassLoader子类。

**拓展类加载器(Extension Class Loader):**负责加载Java平台中扩展功能的一些jar包,包括<JAVA_HOME>\lib\ext 目录中 或 java.ext.dirs 指定目录下的jar包。由Java代码实现。

**应用程序类加载器(Application Class Loader):**我们自己开发的应用程序,就是由它进行加载的,负责加载ClassPath路径下所有jar包。

双亲委派模型

高端的食材往往只需要最简单的烹饪方式,而保证Java程序稳定运行的双亲委派模式,其实也非常简单:

双亲委派模式其实一句话就可以说清楚:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。

ClassLoader 类中有示例,如下:

复制代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先要保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // 先判断这个类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 有父类,优先交给父类尝试加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载失败,这里捕获异常,但不需要做任何处理
            }

            if (c == null) {
                // 没有父类,或者父类无法加载,尝试自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型好处是什么?

在解答这个问题前,需要先了解一个知识点:不同的类加载器,加载同一个类,结果是虚拟机里会存在两份这个类的信息,所以当判断这两个类是否"相等"时,必定是不相等的。

使用双亲委派模式,可以保证,每一个类只会有一个类加载器。例如Java最基础的Object类,它存放在 rt.jar 之中,这是 Bootstrap 的职责范围,当向上委派到 Bootstrap 时就会被加载。

但如果没有使用双亲委派模式,可以任由自定义加载器进行加载的话,Java这些核心类的API就会被随意篡改。

相关推荐
怡人蝶梦2 小时前
Java后端技术栈问题排查实战:Spring Boot启动慢、Redis缓存击穿与Kafka消费堆积
java·jvm·redis·kafka·springboot·prometheus
瓯雅爱分享2 小时前
MES管理系统:Java+Vue,含源码与文档,实现生产过程实时监控、调度与优化,提升制造企业效能
java·mysql·vue·软件工程·源代码管理
鬼多不菜3 小时前
一篇学习CSS的笔记
java·前端·css
深色風信子3 小时前
Eclipse 插件开发 5.3 编辑器 监听输入
java·eclipse·编辑器·编辑器 监听输入·插件 监听输入
Blossom.1183 小时前
人工智能在智能健康监测中的创新应用与未来趋势
java·人工智能·深度学习·机器学习·语音识别
shangjg33 小时前
Kafka 如何保证不重复消费
java·分布式·后端·kafka
无处不在的海贼3 小时前
小明的Java面试奇遇之互联网保险系统架构与性能优化
java·面试·架构
Layux4 小时前
flowable候选人及候选人组(Candidate Users 、Candidate Groups)的应用包含拾取、归还、交接
java·数据库
Mylvzi4 小时前
Spring Boot 中 @RequestParam 和 @RequestPart 的区别详解(含实际项目案例)
java·spring boot·后端