Java类加载概述

本文详细介绍了Java类加载机制的五个阶段:加载、验证、准备、解析和初始化,以及类加载的时机。类加载过程包括加载、连接(验证、准备、解析)和初始化。类加载器的三层结构(启动类加载器、拓展类加载器、应用程序类加载器)和双亲委派模型也在文中被讨论,该模型保证了系统类的唯一性并防止危险代码注入。
摘要由CSDN通过智能技术生成

一、简介

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

​ 类的生命周期会经历如下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。

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

二、类加载的时机

​ 其实就是这个.class文件什么时候呗读取到虚拟机的内存中,并且达到可用的状态。

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

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

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

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

  • 反射调用时,例如Class.forName(com.xxx.YyyYyy);

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

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

三、类的加载过程

​ 类的加载过程其实也可以说成是:加载、连接、初始化,其中连接就包含了验证、准备和解析。如下图:

在这里插入图片描述

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

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

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

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

Class文件中的内存是字节码,这些内容可以由任何途径产出,验证的摸底是保证文件内容里的字节流符合Java虚拟机的规范。

主要包括以下校验:

  • 文件格式的校验:例如是否以0xCAFEBABE开头等等;
  • 元数据的验证:对字节码描述的元数据信息进行语义分析,要符合规范,例如是否继承了不可集成的类等等;
  • 字节码的验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的;
  • 符号引用的验证:发生在解析阶段,符号引用转直接引用的时候,例如:确保符号引用的全限定名能找到对应的类等等。

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

3、准备

这个阶段,类的静态字段信息,会得到内存分配,并且设置为初始值。对于这个阶段,需要注意以下几点:

  • 内存分配仅包括static修饰过的变量,而不包括实例变量,实例变量得等到对象实例化时分配内存;
  • 设置为初始值时,初始值指的是变量数据类型的默认值,而不是在Java代码中显式赋予的值,但是,当字段被final修饰时,这个变量就成为了常量,那么这个初始值就是代码中显式赋予的值;
//例如
public static int i = 3;
//在准备阶段这个时候,i变量的初始值是0,不是3,把i赋值为3的操作将在初始化阶段才会执行

//如果被final修饰
public static final int i = 3;
//那在准备阶段设置的初始值是 3,不是 0。
  • 在Java8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java 堆中。
4、解析

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

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

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

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

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

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

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

<clinit>()方法的作用是什么?
上述准备阶段时,已经堆类中的static修饰的变量赋予了初始值,
<clinit>()方法的作用就是给这些变量赋予代码中实际定义的值时类中如果存在static代码块,
也会执行这个静态代码块中里面的内容。
6、加载过程总结

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

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

四、类加载器

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

1、三层类加载器介绍
  • 启动类加载器:Bootstrap Class Loader,负责加载<JAVA_HOME>/lib目录,或者被-Xbootclasspath参数指定的路径,例如jre/lib/rt.jar里面所有的class文件。由C++实现。
  • 拓展类加载器:Extension Class Loader,负责加载Java平台中扩展功能的一些jar包,包括<JAVA_HOME>/lib/ext目录中或java.ext.dirs指定目录下的jar包。由Java代码实现。
  • 应用程序类加载器:Application Class Loader,我们自己开发的应用程序就是由它进行加载的,负责加载ClassPath路径下所有的jar包。
2、双亲委派机制(模型)

​ 任何一个类加载器在接到一个类的加载请求时,都会先让其父类(上级类加载器)进行加载,只有父类无法加载(或者是没有上一级的类加载器)的情况下,才会尝试自己加载。

在这里插入图片描述

在这里插入图片描述

从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?

为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

五、总结

在这里插入图片描述
主要参考以下文章:
https://zhuanlan.zhihu.com/p/25228545
https://blog.csdn.net/codeyanbao/article/details/82875064

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值