现如今,各种IDE越来越智能,我们程序员的日常开发基本上都是在IDE上完成的,它可以帮助我们将更多的注意力放在实际的业务处理中,随着这种安逸的编码生活的持续,我们慢慢也就忘记了代码运行的底层原理。如果不学习,好像也没啥问题,毕竟我们的关注重点是代码逻辑实现上,当出现问题了,百度,谷歌一下,或者问问公司的狠人,问题好像也能愉快的解决,自己好像也理解了似的。但事实上呢,依此周而复始,仍旧不理解,学习一门技术,只有我们真正懂得了其底层原理,才能更好的解决问题。
我们在前面几篇文章中分别讲解了类文件结构,JVM内存管理。这两篇文章详细描述了Class文件存储格式的具体细节及JVM运行时数据区。而今天这篇文章将会讲解Class文件中的信息进入到虚拟机中会发生什么变化。
**先来个官方叙述:**类加载是Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化、最终形成可以被虚拟机直接使用的Java类型。通俗来讲,就是我们在完成代码的编写后,编译器会将我们的java文件编译成对应的class文件(二进制字节码文件),通过类载器将这些class的时候将其加载到JVM中,生成对应的class对象。下面,让我们详细来分析下类加载过程。
对于任意一个类,类加载过程可以分为加载
、验证
、准备
、解析
、使用
和卸载
七个阶段,如下图所示:
image-20210128214026739
图中的加载
、验证
、准备
、初始化
和卸载
这五个阶段的顺序是确定的,而解析
则不一定,为了支持java语言的运行时绑定特性
,解析这个阶段可以发生在初始化阶段后。接下来我们详细分析类加载过程中这几个模块的作用。
类加载
阶段是将字节码文件.Class的二进制数据读入内存中的方法区中,然后在堆中创建一个Java.lang.Class对象
,对于加载阶段的任意一个类都对应着一个Class类型的对象,可以通过getClass()
来获取。对于确定的类Class,无论该类生成多少个对象,其Class类型的对象只有一个,Class类是整个反射的入口。
因此,在类加载阶段,Java虚拟机主要完成以下几类任务:
验证是连接阶段的第一步,其目的是为了确保Class文件内的字节流包含的信息符是否符合Java虚拟机规范的要求,保证输入的字节流不会危害到虚拟机自身的安全。我们也许会有疑问,我们印象中的Java语言是一门相对安全的语言啊(相比较于C++),如单纯的使用Java代码是无法访问到边界以外的数据,如果我们非要这么做,编译器就会拒绝编译。但是,回到字节码层面,一切都变得不可控起来,这是因为Class文件可以采用很多途径来产生,并不一定要求用Java源码编译出来,如果JVM虚拟机不检查输入的字节流,对其完全信任的话,很可能就会因为载入有害的的字节流导致系统的崩溃。因此,验证阶段在类加载过程中占有很大的比重,它验证的项目可以大致分为以下几个:文件格式的验证、元数据验证、字节码验证和符号引用验证,下面我们一一介绍:
文件格式的验证就是检查字节流是否符合Class文件格式的规范,不熟悉Class文件格式的可以看我的上一篇文章类文件结构,文件格式通常检查一下几个要素:
元数据的验证是对字节码描述的信息进行语义分析,验证的要素主要包含以下几点:
字节码验证是整个验证过程中的最复杂的一个阶段,它主要通过数据流
和控制流
分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件,例如:
符号引用验证可以看做是对类自身(常量池中的各种符号引用)的信息进行匹配性校验
,它的目的是确保解析动作能够正常执行,如果无法通过符号的引用验证,则会抛出异常。符号引用验证阶段通常需要校验以下内容:
......
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量)
,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。这里所说的初始值“通常情况”下是数据类型的零值。
public static int number=10
类变量number在准备阶段值是0而不是10,因为这时候尚未开始执行任何Java方法,而把number赋值为10的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把number赋值为10的动作将在初始化阶段才会执行。下表列出了所有Java基础类型的零值:
解析阶段就是将Class中的常量池中的符号引用
解析为直接引用
。符号引是使用一组符号描述被引用的目标,引用的目标不一定加载到内存中;直接引用可以使直接指向目标地址的指针,相对偏移量或者间接定位到目标的句柄,有了直接引用,引用的目标一定存在在虚拟机中。主要包括四种类型引用的解析,分别是类或接口解析、字段解析、方法解析和接口方法解析。下面以字段解析和方法解析为例:
初始化是类加载过程的最后一步,到了初始化阶段,才开始正真的执行字节码文件,根据字节码文件的内容对类的各个字段进行赋值;初始化是执行类构造器()方法的过程。实际上,在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举例如下:
public static int number1 = 5;
public static int number2 = 6;
static{
number = 68;
}
在准备阶段number1和number2都等于0;在初始化阶段number1和number2分别等于5和6。
总结一下初始化发生的条件:
使用阶段是当执行完初始化后,就可以根据自己的实际需要使用具体的类;当我们在程序中执行System.exit(),加载的类会从内存中卸载,通常情况下,当程序正常执行结束后、或者发生错误而终止都会使得已加载的类对象被卸载。
通过以上的讲解,我们知道了类Class文件被虚拟机加载、使用直至卸载需要经历的步骤,但是我们忽略了一个非常重要的问题,类是如何被加载器加载的,加载器需要满足什么样的规律?下面我们一一来讲解。
类的加载是使用类加载器通过查询路径的方式进行的,加载阶段既可以使用虚拟机里内置的引导类加载器来完成,也可以由用户自定义类加载器来完成Java中的类加载器通常分为四类:启动类加载器
(Bootstrap ClassLoader)、扩展类加载器
(Extension ClassLoader)、应用程序类加载器
(Application ClassLoader)、用户自定义类加载器
(User ClassLoader)。不同的类加载器负责不同区域的类的加载。
上面我们讲到不同的类加载器都有不同的加载范围,当某个类加载器要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。因此,不同类加载器相互配合就形成类双亲委派模型。
我们先分析以下加载流程:
我们在上图可以看到,除了启动类加载器,每一个类加载器都有一个父类加载器。当一个类加载器加载一个类时,首先会把加载动作委派给他的父加载器,如果父加载器无法完成这个加载动作时才由该类加载器进行加载。由于类加载器会向上传递加载请求,所以一个类加载时,首先尝试加载它的肯定是启动类加载器(逐级向上传递请求,直到启动类加载器,它没有父加载器),之后根据是否能加载的结果逐级让子类加载器尝试加载,直到加载成功。
双亲委派模型的作用:
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。 参考文献:
[1]周志华.深入理解Java虚拟机(第三版)
[2]https://blog.csdn.net/en_joker/article/details/79959330
[3]https://www.cnblogs.com/aspirant/p/7200523.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。