类加载机制

虚拟机把描述类的数据从Class文件(一串二进制字节流)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是java的类加载机制

加载时机

类从被加载到虚拟机内存到卸载出内存为止,生命周期包括加载(Loading),验证(Verification)准备(Preparation)解析(Resolution)初始化Initialization使用(Using)卸载(Unlaoding),如下图途所示

Tips:其中加载、验证、准备、初始化和卸载这5个顺序是确定的,而解析阶段不一定,它可以在初始化阶段之后才开始,这是为了支持java语言的运行时绑定。

触发时机

1、加载(Loading)

java虚拟机规范没有做强制约束,由虚拟机具体实现自由把握。

2、初始化(Initialzation)

主动引用必须初始化,有5种

遇到new、getstatic、putstatic或者invokestatic这4条字节码指令,若类没有进行过初始化则需要触发其初始化

使用java.lang.reflect包的方法对类进行反射调用,若类没有进行过初始化则需要触发其初始化

当初始化一个类时候如果发现其父类还没有进行过初始化,则需要触发其父类的初始化

当虚拟机启动时,用户需要制定一个要执行的类(包含main方法),虚拟机回先初始化这个主类

当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例解析后的解析结果是REF_getStatic、REF_putStatic、REF_invokeSatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要触发其初始化。

Tips 被动引用

通过子类引用父类的静态字段,不会导致子类初始化

使用数组定义来引用类,不会触发此类的初始化

常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化

类加载过程

类加载包括5个部分:加载、验证、准备、解析、初始化

加载

主要完成3件事:

通过一个类的全限定名称来获取定义此类的二进制字节流(获取字节流有多种方式如zip包读取、网络中获取等)

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口

验证

验证是链接阶段的第一步,这个阶段目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全

Tips:Class文件并不一定要求用Java源码编译来产生,可以使用任何途径产生,包括十六进制编译器直接生成,虚拟机若是不检查输入字节流对其完全信任很可能会载入有害字节流而导致系统奔溃。

验证阶段包括如下四个阶段

1、文件格式验证

第一阶段,主要包括字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面的三个阶段均是基于方法区的存储结构进行,不会再直接操作字节流

2、元数据验证

第二阶段,是对字节码描述的信息进行语义分析,以确保其描述信息符合java虚拟机规范要求

3、字节码验证

第三阶段,是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析确定语义是合法合逻辑的,在第二阶段数据类型校验校验完成后,这个阶段将对类的方法进行校验,保证类的方法在运行时不会做出危害虚拟机安全的事情

4、符号引用验证

最后一个阶段发生在虚拟机将符号引用转化为直接引用时,这个转化动作发生在解析阶段

Tips:验证阶段是一个非常重要的但是不是一定必要的的阶段。如果运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证准备,以缩短虚拟机加载的时间

准备

准备阶段是正式为类变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中,其次这个阶段初始值是数据类型的零值。
public static int value=123; value变量在准备阶段后初始值为0而不是123,在value赋值为123的putstatic指令是被程序编译后,存放在()方法之中,因此value阶段赋值为123的动作将在初始化阶段才会执行。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄

直接引用和间接引用是和虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不同。如果有了直接引用,那引用的目标一定在内存中存在

初始化

类加载过程最后一步,此阶段才是真正执行类中定义的java程序代码

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”,这个动作放到java虚拟机的外部实现,以便让程序决定如何去获取所需要的类,实现这个动作的代码模块称之为“类加载器”。即是用于从Class文件加载所需的类,主要场景用于热部署、代码热替换等场景。

Tips:对于任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,每个类加载器都有一个独立的类名称空间。即比较两个类是否相等只有两个类由同一个类加载器前提才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类也不同。

加载器分类

在虚拟机看来仅存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLaoder)这个类加载器使用C++实现是虚拟机的一部分;另一种就是其他所有的类加载器,这些加载器都由java语言实现,独立于虚拟机外部并且都继承抽象类java.lang.ClassLoader

系统提供3种的类加载器:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader
1、Bootstrap ClassLoader

启动类加载器,一般由C++实现,是虚拟机的一部分。该类加载器主要职责是将JAVA_HOME路径下的\lib目录中能被虚拟机识别的类库(比如rt.jar)加载到虚拟机内存中。Java程序无法直接引用该类加载器

2、 Extension ClassLoader

扩展类加载器,由Java实现,独立于虚拟机的外部。该类加载器主要职责将JAVA_HOME路径下的\lib\ext目录中的所有类库,开发者可直接使用扩展类加载器。 该加载器是由sun.misc.Launcher$ExtClassLoader实现。

3、 Application ClassLoader

应用程序类加载器,该加载器是由sun.misc.Launcher$AppClassLoader实现,该类加载器负责加载用户类路径上所指定的类库。开发者可通过ClassLoader.getSystemClassLoader()方法直接获取,故又称为系统类加载器。当应用程序没有自定义类加载器时,默认采用该类加载器。

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,一般采用组合来复用父加载器代码。

执行过程:当一个ClassLoader收到来类加载的请求,首先把该请求委派该父类ClassLoader处理,当父类ClassLoader反馈自己无法完成这个加载请求时,才由当前类ClassLoader来处理。对于每个ClassLoader这都是如此,也就是父类的优先于子类处理类加载的请求,那么也就是说任何一个请求第一次处理的便是最顶层的Bootstrap ClassLoader(启动类加载器)。

若没有这种机制,用户自己编写如java.lang.Object的类放在ClassPath中,并放在ClassPath中系统就会出现不同的Object类,应用程序会引起混乱。双亲委派模型对于保证java程序稳定运行很重要。实现代码集中在java.lang.ClassLoader的loadClass方法中,逻辑如下:先检查是否已经加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。若父加载器加载失败,抛出异常后没在调用自己的findClass方法加载。

破坏双亲委派

由于双亲委派模型并不是一个强制性约束模型,而是java设计者推荐给开发者的类加载方式。但是也有例外,双亲委派模型主要出现3次较大规模的“被破坏”情况

1、在双亲委派模型引入前即jdk1.2前,为了兼容之前jdk,jdk1.2之后java.lang.ClassLoader添加了新的protected方法findClass(),在此之前用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadclass方法。jdk1.2以后不提倡用户再去覆盖loadclass方法,而应把自己的类加载逻辑放到findClass方法中,在loadClass方法逻辑中如果父类加载失败,则会调用自己的findClass方法来完成加载。

2、模型本身缺陷,双亲委派很好的解决了各个类加载器基础类的统一问题,但是基础类要是需要回调用户的代码这样就容易出问题了,典型的就是JNDI服务(JNDI目的是对资源进行集中管理查找,需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者,但是启动类加载器不认识这些代码,为了解决这个问题引入了“线程上下文加载器”)

3、对程序动态性的追求而导致,如热部署等。

权且当一个读书笔记吧~

参考《深入理解java虚拟机》

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐