我们知道,我们写的java文件是不能直接运行的,我们可以在IDEA中右键文件名点击运行,这中间其实掺杂了一系列的复杂处理过程。这篇文章,我们只讨论我们的代码在运行之前的一个环节,叫做类的加载。
首先我们给出这篇文章的大致结构:
首先,认识类加载机制;
然后,详细介绍类加载的过程。
最后,说一说类加载器,以及双亲委派原则。
1.什么是类的加载
在介绍类的加载机制之前,先来看看,类的加载机制在整个java程序运行期间处于一个什么环节,下面使用一张图来表示:
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器的作用其实就是类的加载。今天我们要讨论的就是这个环节。有了这个印象之后我们再来看类的加载的概念:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
到现在为止,我们基本上对类加载机制处于整个程序运行的环节位置,还有类加载机制的概念有了基本的印象。在类加载.class文件之前,还有两个问题需要我们去弄清楚:
- 在什么时候才会启动类加载器?
其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。 - 从哪个地方去加载.class文件
在这里进行一个简单的分类。例举了5个来源
(1)本地磁盘
(2)网上加载.class文件(Applet)
(3)从数据库中
(4)压缩文件中(ZAR,jar等)
(5)从其他文件生成的(JSP应用)
2.类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
下面就一个一个去分析一下这几个过程。
2.1 加载
”加载“是”类加机制”的第一个过程,它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。
加载可以理解为收简历,OK,公司的HR小姐姐会将不同来源的简历(可能有算法工程师的、可能有后台运营的,也有前端或者测试的。代表了不同的数据源)集中起来修改成人力部能识别的简历样式。当然你怎么修改无所谓,只要最后人力部能看明白就可以。(也就是可以选择不同的类加载器进行加载)
2.2 验证
这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
验证可以理解为简历筛选,OK,公司要求应聘者需要具备211硕士以上学历,否则不予录用。当然,验证的时候可能会筛下去很多不符合条件的简历,万一简历数量没有达到公司要求就不好了,所以可能会涉及到收更多的简历。
2.3 准备
创建类或接口中的类/静态变量,并初始化类/静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
首先,我们得知道什么叫做静态变量:静态变量是一个公共的存储单元,所以类的任何一个对象去修改它时,都是在对同一个内存单元去操作。而实例变量都有自己独立的内存来保存它的值,且不与其他对象共享。
在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
- 类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中。(这里复习以下,还记得什么叫做类加载吧?类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。所以类变量被认作为二进制数据,会被存入内存,而实例变量被认作是数据结构的一部分,会被分配到java堆中。)
- 这里的初始值指的是数据类型默认值,即将基本数据类型设置为0,将boolean设置为false,将引用设置为null。而不是代码中被显示赋予的值。比如
public static int value=1;
//在这里,准备阶段过后的value值为0,而不是1。赋值为1的动作在初始阶段。
OK,后面三个编不出来了。。。。。。。
2.4解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。
2.5 初始化
这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。
这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
JVM初始化步骤
-
假如这个类还没有被加载和连接,则程序先加载并连接该类
-
假如直接父类还没有被初始化,则先初始化其直接父类
-
假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
-
创建类的实例,也就是new的方式
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(如 Class.forName(“com.shengsiyuan.Test”))
-
初始化某个类的子类,则其父类也会被初始化
-
Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。
3.类加载器
Java语言系统自带有三个类加载器
Bootstrap ClassLoader启动类加载器 :加载jre/lib下的jar文件。
Extention ClassLoader扩展类加载器 :jre/lib/ext/目录下的jar文件。
Appclass Loader应用类加载器:也称为SystemAppClass。 加载当前应用的classpath的所有类。
我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
这就是双亲委派模型:
他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
可以避免重复加载,父类已经加载了,子类就不需要再次加载,但子类加载器可以访问父类加载器的类型。
更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
代码验证一下:
public class ClassLoaderTest {
public static void main(String[] args){
ClassLoader loader=Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
运行结果:
从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
来源:CSDN
作者:云计算小菜鸟
链接:https://blog.csdn.net/weixin_41724265/article/details/104628684