前言
我们已经了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。而虚拟机如何加载这些Class文件?这些信息进入到虚拟机后会发生什么变化?。这是我们接下来要学习的内容。
虚拟机把描述类的数据从calss文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。与那些在编译期需要进行连接的工作语言不同,在Java语言里,类加载荷连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为程序提供高度的灵活性,动态扩展的特性也就是依赖运行期间动态加载和动态连接这个特点实现的。如面向接口的编程。
类加载的时机
类从加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期共包含七个阶段,如下图:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
初始化规范
虚拟机规范中堆什么情况需要开始类加载的第一个阶段,加载并没有强制约束,但是对初始化阶段,规范了有且只有4种情况必须立即对类进行初始化。
- 遇到new、getstatic、putstatic或者invokestatic这4条字节码指令时(比如在使用new创建对象的时候,读取或设置一个类静态字段、调用一个类的静态方法)
- 使用java.lang.relect包的方法对类进行反射调用的时候,如果类没有进行初始化,首先出发初始化。
- 当初始化一个类,发现父类还没有进行初始化,先出发父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的哪个类),虚拟机会先初始化这个主类。
以上四种是对类的主动引用,下面说几种被动引用:
- 通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
- 一个类的成员进入常量池,另一个类引用只是对常量池的引用,两个class就无关了。
- 接口加载和类加载稍微有点不同,只有真正使用到父接口的时候才会初始化。
类加载的过程
接下来讲解一下类加载的全过程,也就是加载、验证、准备、解析和初始化这五个过程。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法去访问数据的访问入口。
正是因为没有指明具体的从哪获取,怎么获取,所以充满活力的开发前辈玩出了很多花样:
- 从zip包中读取,最后成为jar,EAR,WAR格式的基础。
- 从网络中获取,比如Applet
- 从计算机运行生成,比如动态代理
- 其他文件,比如JSP
- 从数据库读取,有些中间件服务器可以把程序安装到数据库中来完成程序代码在集群间的分发。
相对其他过程,加载是开发期可控性最强的阶段。可以使用系统的加载器,也可以自定义类加载器。
验证
- 文件格式验证;比如魔数、主次版本号;常量池是否含有不支持的常量类型;等等。。。经过验证才会进入内存的方法区进行存储。所以下面的几个验证阶段全部都是基于方法区的存储结构进行的。
- 元数据验证;验证点比如:这个类是否有父类,父亲是否成常量不允许基础的类,如果这个类不是抽象类,是否实现了父类或接口之中要求实现的索引方法,类中的字段、方法是否和父类产生了矛盾等。这一阶段主要进行语义检验,保证不存在不符合Java语言规范的元数据信息。
字节码检验;是最复杂的的一个阶段,主要工作是进行数据流和控制流分析。在第二个阶段堆数据类型做完校验之后,这阶段对类的方法体进行校验分析。比如:保证任何时刻操作数类型和指令码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令、保证类型转换时有效的。如果通过不一定安全,不通过一定不安全。
符号引用;对类自身以外的信息进行匹配性校验。比如:全限定名能否找到该类,指定类中是否存在符合方法的字段描述符。
准备
此阶段时正式为类变量( 不包括实例变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法四类符号引用。
初始化
对类的静态变量,静态代码块执行初始化操作。
参考这边博文:类加载机制及反射。
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,实现这个动作的代码块称为类加载器。它在Java Applet、类层次划分、OSGi、热部署、代码加密等领域大放异彩。
对任意一个类都需要由加载它的类加载器和这个类本身一同确定它在虚拟机中的唯一性。对虚拟机里来说只有两种不同的类加载器,一种是启动类加载器由C++实现,另一种就是Java写的,都继承java.lang.ClassLoader。对于开发人员来说它还进一步分为扩展类的和应用程序类的加载器。分别加载/lib/etc目录下的,和calsspath用户路径下的。

双亲委派模型
工作过程:如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。在classload的loadclass()方法中实现。
好处:java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
说明
文中出现的图片,文字描述有些来自互联网,但是出处无法考究,如果侵犯您的相关权益,请联系我,核实后我会马上加上转载说明。谢谢!!!