类加载子系统
JVM 架构如下图,接下来将从类加载子系统、运行时数据区来逐步讲解 JVM 虚拟机
类加载的时机
类加载的时机主要有 4 个:
- 遇到
new、getstatic、putstatic、invokestatic
这四条字节码指令时,如果对应的类没有初始化,则要先进行初始化
- new 关键字创建对象时
- 读取或设置一个类型的静态字段时(被 final 修饰、已在编译器将结果放入常量池的静态类型字段除外)
- 调用一个类型的静态方法的时候
- 对类进行
反射调用
时 - 初始化一个类的时候,如果其父类未初始化,要先初始化其父类
- 虚拟机启动时,要先加载主类(程序入口)
类加载过程
类的生命周期如下图:
- 加载
- 通过二进制字节流加载 class 文件
- 创建该 class 文件在方法区的运行时数据结构
- 创建字节码对象 Class 对象
- 链接
- 验证:目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证 - 准备:为类变量(即静态变量)分配内存并且设置类变量的默认初始值,即零值。
这里不包含用 final 修饰的 static 变量,因为 final 修饰的变量在编译为 class 字节码文件的时候就会分配了,准备阶段会显式初始化
这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到 Java 堆中 - 解析:将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的莫表。符号引用的字面量形式明确定义在《java虚拟机规范》的Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
- 初始化
虚拟机在初始化阶段才真正开始执行类中编写的 Java 程序代码
初始化阶段就是执行类构造器<clinit>()
方法的过程,<clinit>()
是 Javac 编译器自动生成的,该方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并生成的,如果一个类中没有静态代码块, 也没有变量赋值的动作,那么编译器可以不为这个类生成<clinit>()
方法
类加载器
JVM 中类加载是通过类加载器来完成的
- 启动类加载器(Bootstrap ClassLoader):
- 负责加载
JAVA_HOME\lib
目录中的,或通过-Xbootclasspath
参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。由 C++ 实现,不是 ClassLoade r的子类
- 扩展类加载器(Extension ClassLoader):
- 负责加载
JAVA_HOME\lib\ext
目录中的,或通过java.ext.dirs
系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):
- 负责加载用户路径
classpath
上的类库
- 自定义类加载器(User ClassLoader):
- 作用:JVM自带的三个加载器只能加载指定路径下的类字节码,如果某些情况下,我们需要加载应用程序之外的类文件,就需要用到自定义类加载器
双亲委派机制
加载类的class文件时,Java虚拟机采用的是双亲委派机制
,即把请求交给父类加载器去加载
工作原理:
- 如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器也存在其父类加载器,则继续向上委托
- 如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成类加载任务,则会由自家在其尝试自己去加载
优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改(例如,如果我们自定义一个java.lang.String类,然后我们去new String(),我们会发现创建的是jdk自带的String类,而不是我们自己创建的String类)
为什么还需要破坏双亲委派?
- 在实际应用中,可能存在 JDK 的基础类需要调用用户代码,例如:SPI 就打破双亲委派模式(打破双亲委派意味着上级委托下级加载器去加载类)
- 比如,数据库的驱动,Driver 接口定义在 JDK 中,但是其实现由各个数据库的服务上提供,由系统类加载器进行加载,此时就需要
启动类加载器
委托子类加载器去加载 Driver 接口的实现