类的加载是每个Java程序必经的流程,了解类的加载机制有助于我们写出更好的代码。
基础
概念
类的加载把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的。显然,这样会增加一些运行时的开销,不过也为Java应用程序提供了高度的灵活性,使Java天生支持动态扩展。
类的生命周期
类的生命周期有7部分:加载,验证,准备,解析,初始化,使用和卸载。
其中验证,准备,解析3个部分统称为连接。
具体过程
加载
加载(Loading)是类加载的第一步,就是将字节码加载到JVM内存中。官方说法如下:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注:JVM在加载数组的时候加载的仅仅是数组的类型类(例如String[] 加载器只会加载String这个类型类),而数组的创建则由JVM直接完成。
类加载器
为了完成加载过程中的第一条:”通过一个类的全限定名来获取定义此类的二进制字节流”的功能,JVM团队开发了一个模块——类加载器。但是为了给用户提供更好的拓展性JVM团队将这个过程的代码放到了JVM的外部,以便让开发人员可以自定义类加载器。
注:对于任意一个类,他的唯一决定方式是:类本身+加载此类的类加载器。通俗的讲就是:一个类java.lang.Object 如果被两个类加载器加载,那么这个两个类就是不相同的。
加载器的类型
从Java虚拟机的角度看,只有两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):用C++实现,是虚拟机自身的一部分;
- 所有其他的类加载器:用Java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader;
从Java开发人员看,类加载器可分为3种:
- 启动类加载器(Bootstrap ClassLoader):负责加载<\JAVA——HOME>\lib目录中的并且可以被虚拟机识别的;
- 扩展类加载器(Extension ClassLoader):负责加载<\JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器;
- 应用程序类加载器(Application ClassLoader):它是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库。
双亲委派模型
那么,问题来了,如果用户自定义了一个Object类放在classpath目录下,那么加载的时候会用哪个加载器呢?为了解决这个问题,JVM团队就提出了双亲委派模型。
首先我们要知道,除了启动类加载器以外,其余的加载器都有自己的父类加载器。继承关系如下图:
双亲委派模型就是,如果一个类加载器收到加载请求,他不会马上加载类,而是将这个请求向上传递给他的父加载器,看父加载器能不能加载这个类,加载的原则就是优先父加载器加载,若果父加载器加载不了,自己才能加载。
验证
验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
主要有4个部分:
a. 文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
b. 元数据验证
对类的元数据信息进行语义校验,是否不存在不符合Java语言规范的元数据信息
c. 字节码验证
最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
d. 符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三个阶段——解析阶段中发生。
准备
类的准备阶段负责为类的静态变量分配内存,并设置默认初始值。
解析
解析是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化是类的加载的最后一步,会执行类构造器<clinit>()方法。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
简单点说初始化就是为类变量进行赋值和执行静态代码块。