写在最前
Java 类加载器(ClassLoader
)它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。根据一个类的全限定名来读取此类的二进制字节流
到 JVM 中,然后转换为一个与目标类对应的 java.lang.Class 对象实例。
对于任意一个类,都必须由加载它的类加载器
和这个类本身
一起共同确立其在 Java 虚拟机中的唯一性
,每一个类加载器,都拥有一个独立的类名称空间。
类的生命周期
Java 类加载需要经历以下 7 个过程:
由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化
。
加载
加载是类加载的第一个过程,在这个阶段,将完成以下三件事情:
- 通过一个类的全限定名获取该类的二进制流。
- 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
- 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
验证
验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机。在该阶段主要完成以下四种验证:
- 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
- 字节码验证:整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
- 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的 内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
/**
* 在准备阶段 value 初始值为 0。
* 在初始化阶段才会变为 123 。
*/
public static int value=123;
解析
该阶段主要完成符号引用到直接引用的转换动作。
初始化
到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。为类的静态变量赋予正确的初始值。
- 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
- 如果类中存在初始化语句,就依次执行这些初始化语句。
使用
new出对象程序中使用。
卸载
执行垃圾回收。
类加载器分类
JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。
启动类加载器
Bootstrap ClassLoader:负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME\jre\lib (JAVA_HOME 代表 JDK 的安装目录,下同) 文件夹中,如 rt.jar(runtime)、i18n.jar 等,这些是 Java 的核心类。这个 ClassLoader 比较特殊,它是由 c/c++ 代码实现的,我们将它称之为「根加载器」。
我们常用内置库 java.xxx. 都在 rt.jar 中,比如:java.util.、java.io.、java.nio.、java.lang. 等。
扩展类加载器
Extension ClassLoader:负责加载 JVM 扩展类,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 JAVA_HOME\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.* 开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器
Application ClassLoader:面向我们用户的加载器,该类加载器由 sun.misc.Launcher$AppClassLoader 实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。
自定义类加载器
因为系统的 ClassLoader 只会加载指定目录下的 class 文件,如果你想加载自己的 class 文件,那么就可以自定义一个ClassLoader。而且我们可以根据自己的需求,对 class 文件进行加密和解密。
双亲委派机制
某个特定的类加载器
在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,子加载器才会尝试自己去加载。
模式优势
- Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类 ClassLoader 再加载一次。
- 考虑到安全因素,Java 核心 Api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心API库被随意篡改。
打破双亲委托机制的案例
Java默认是双亲委托机制,当然开发中也有很多打破双亲行为的案例,比如:
- Tomcat 可以加载自己目录下的 class 文件,并不会传递给父类的加载器。
- Java 的 SPI,发起者是 BootstrapClassLoader,BootstrapClassLoader 已经是最上层的了。它直接获取了 AppClassLoader 进行驱动加载,和双亲委派是相反的。
JVM 中对象的创建过程
1、拿到内存创建指令
当虚拟机遇到内存创建的指令的时候(new 类名),来到了方法区,找根据 new 的参数在常量池中定位一个类的符号引用。
2、检查符号引用
检查该符号引用有没有被加载、解析和初始化过,如果没有则执行类加载过程,否则直接准备为新的对象分配内存。
3、分配内存
虚拟机为对象分配内存(堆)分配内存分为指针碰撞和空闲列表两种方式。
指针碰撞
所有的存储空间分为两部分,一部分是空闲,一部分是占用,需要分配空间的时候,只需要计算指针移动的长度即可。
空闲列表
虚拟机维护了一个空闲列表,需要分配空间的时候去查该空闲列表进行分配并对空闲列表做更新。可以看出,内存分配方式是由 Java堆是否规整决定的,Java 堆的规整是由垃圾回收机制来决定的。
4、初始化
分配完内存后要对对象的头(Object Header)进行初始化,这些信息包括:该对象对应类的元数据、该对象的GC代、对象的哈希码。
抽象数据类型默认初始化为 null,基本数据类型为0,布尔为 false…
5、调用对象的初始化方法
也就是执行构造方法。