还不知道Java类加载机制,你算白学了

1
前言

在Java的世界里,每一个类或者接口,在经历编译器后,都会生成一个个.class文件。类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。

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

2
类加载的时机

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

但严格意义上来说,加载和初始化,是类生命周期的两个阶段。对于什么时候加载,Java虚拟机规范中并没有约束,各个虚拟机都可以按自身需要来自由实现。但绝大多数情况下,都遵循“什么时候初始化”来进行加载。

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

Ø使用new实例化对象时、读取或者设置一个类的静态字段或方法时

Ø反射调用时,例如 Class.forName(“com.xxx.TestDemo”)

Ø初始化一个类的子类,会首先初始化子类的父类

ØJava虚拟机启动时标明的启动类

ØJDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化

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

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

还不知道Java类加载机制,你算白学了

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

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

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

还不知道Java类加载机制,你算白学了 

 1.加载

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

Ø通过一个类的全限定名去找到其对应的.class文件

Ø将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构

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

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

4.解析

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

什么是符号引用?

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

什么是直接引用?

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

引用的类型,还未加载初始化怎么办?

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

还不知道Java类加载机制,你算白学了

5.初始化

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

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

() 方法的作用是什么?

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

() 方法是什么?

() 方法 和 方法是不同的,它们一个是“类构造器”,一个是实例构造器。Java虚拟机会保证子类() 方法在执行前,父类的 () 已经执行完毕。而 方法则需要显性的调用父类的构造器。

() 方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成()方法。

加载过程总结

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

方法区中,使用同一加载器的情况下,每个类只会有一份Class字节流信息;

Java堆中,使用同一加载器的情况下,每个类只会有一份 java.lang.Class 类的对象。

 

4
类加载器

还记得在前面加载阶段,通过类的全限定名,获取该类字节流数据的这个动作么,类加载器就是用来实现这个动作的。当年为了满足浏览器上 Java Applet 的需求,Java的开发团队设计了类加载器,它独立于Java虚拟机外部,允许程序员按自身需要自行实现类加载器。这是一项非常优秀的创新,它让同一个类可以实现访问隔离、OSGi、程序热部署等等。发展至今,类加载器已经是Java技术体系的一块重要基石。

三层类加载器介绍

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

 

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

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

双亲委派模型

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

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

还不知道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就会被随意篡改。

还不知道Java类加载机制,你算白学了

好了,Java类加载机制掌握这些就足够了,再也不用怕面试官问到这类问题了,可以和他掰扯一下了,你们学会了吗?


0 0 投票数
文章评分

本文转载自涤生-林哥 涤生大数据,原文链接:https://mp.weixin.qq.com/s/g0FWdflZjCYGrpYIFg4ajw。

(0)
上一篇 2023-07-04 15:41
下一篇 2023-07-10 09:53

相关推荐

订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x