虚拟机类加载机制
参考周志明《深入理解 Java 虚拟机》
参考张龙深入理解 JVM 教程
类加载机制
Java虚拟机将类的数据从Class文件加载到内存中进行一系列的操作最终形成可以直接被Java虚拟机直接使用的类型,这个过程就是虚拟机的类加载机制。
在Java代码中,类的加载、连接与初始化过程都是在程序运行期间完成的。Java程序中.class文件才是能够被运行起来的
类加载的过程
JVM类加载的一般过程顺序为:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析这三个过程统称为连接。
-
加载:查找并加载类的二进制数据,主要是将它们加载到内存当中。
-
验证:确保被加载的类的正确性, 确保class文件中的字节流中包含的信息符合Java虚拟机规范,这样能够防止字节码篡改而危害虚拟机安全。
-
准备:为类的
静态变量
分配内存,并将其初始化为默认值
,很明显这个时候还未创建对象。
public static int value = 123; // 准备阶段会将value初始int类型的默认值0,而不是123
-
解析:把类中的符号引用转化为直接引用
-
初始化:为类的静态变量赋予正确的初始值
public static int value = 123; // 这个过程会将value赋予123
重点介绍类初始化之前先介绍 Java 程序对类的两种使用方式
- 主动使用
- 被动使用
主动使用的方式:
-
创建类的实例
-
访问某个类或接口的静态变量或者对该静态变量赋值
-
调用类的静态方法
-
反射:
Class.forName("com.test.Test")
-
初始化一个类的子类,主动使用该类
-
JVM启动时被标明为启动类的类
-
jdk1.7后开始提供的动态语言支持
除了上述的情况之外一般都可认为被动使用,被动引用不会导致类的初始化
类的初始化:
每个类或接口被Java程序 “首次主动使用”
才会初始化它们,并且被动使用不会导致类的初始化
-
通过子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化,即只会初始化直接定义这个字段的类
-
常量在编译阶段会存入到调用类(使用类)的常量池中,本质上没有主动引用到定义常量的类,因此不会导致类的初始化,这样将编译后的定义常量所在类的.class文件删除后也不会影响调用类的程序运行, 但是如果这个常量是在非编译期确定的,那么它的值就不会放到调用类的常量池中,这时会导致主动使用这个常量所在的类,进而会导致该类的初始化
-
通过数组定义来引用类,不会触发此类的初始化,因为它是由虚拟机动态的自动生成的,直接继承Object的
-
在对一个接口进行初始化的时候,不需要对其父接口进行初始化,即一个接口不会因为它的子接口或实现类的初始化而初始化,只有真正使用父接口的时候(如引用接口所定义的常量的时候)才会初始化,另外初始化一个类的时候,并不会先初始化它所实现的接口
类加载器
加载类的工具, 通过一个类的全限定名(binary name) 来获取描述该类的二进制字节流
双亲委托模型
- 启动类加载器: 主要负责加载加载 Java 的核心类库,主要是
/lib
路径下的rt.jar
- 扩展类加载器: 主要负责加载扩展 Java SE 功能的类, 主要在
/lib/ext
目录下 - 应用程序类加载器: 复杂加载用户类路径(ClassPath) 上所有的类库,一般是程序的默认加载器
双亲委派模型的工作过程:类加载器收到类加载器的请求,但它首先不会自己尝试加载这个类,
而是首先把这个请求交给父加载器取完成,如果父加载器加载不了,自己再尝试加载。
注意:
双亲委派模型除了启动类加载器,其余加载器都有自己的父加载器。
双亲委派模型并非强制约束,在一定情况下可以打破。
类加载器之间的关系是
组合
,可以理解为集合的关系,父加载器是子加载器的子集,一般是非继承
关系。子加载器的类能够访问父加载器的类,而父加载器的类无法访问子加载器的类
知道了类加载器的分类,那么如何知道一个类的类加载器ClassLoader
-
获得当前类的ClassLoader:
class.getClassLoader()
-
获得当前线程上下文的ClassLoader:
Thread.currentThread().getContextClassLoader()
-
获得系统的ClassLoader:
ClassLoader.getSystemClassLoader()
思考:
类加载器和加载其他的类,那么类加载器是怎么加载自身的呢?
AppClassLoader, ExtClassLoader都是java编写的类,这些类的加载都是由BootstrapClassLoader(C++编写,内嵌在JVM中,Java启动时BootstrapClassLoader会加载java.lang.classLoader) 加载的, 启动类加载器不是Java类,其他的加载器都是Java类(继承java.lang.ClassLoader)
除此之外,BootstrapClassLoader还会加载JRE正常运行所需的基本组件,即java.util.和java.lang包中的类
面试题
:双亲委托模型的好处:
-
使类加载器具备一种带有优先级的层次关系,确保Java核心类库的安全,确保Java核心类都是由启动类加载器完成,从而确保核心类不会被用户自定义同名的类的替换
-
不同的类加载器可以为相同名称的类创建额外的命名空间,即一个类可以由用户自定义的不同的类加载器加载多次,那么这些创建的类也是不相同的,而且这些类也是不兼容的,相当于创建了相互隔离的类空间
打破双亲委托模型
SPI,服务提供者接口,这些接口属于Java核心库,由启动类加载器加载,例如JDBC
, Java 提供接口规范,由数据库厂商实现这些接口。而数据库厂商提供的jar包一般都是在ClassPath路径中,而在这个路径中启动类加载器是不能加载的,因此就出现了一种特殊的类加载器:线程上下文类加载器
线程上下文类加载器:
ContextClassLoader
: Thread类中 getContextClassLoader
和setContextClassLoader(ClassLoader cl)
分别用来获得和设置上下文加载器,如果没有通过setContextClassLoader(ClassLoader cl)
进行设置的话,那么会继承父线程的加载器,而Java应用的初始线程类加载器是应用程序类加载器,所以一般这个类加载器就默认是应用程序类加载器
线程上下文类加载器使用的伪代码:
线程上下文类加载器的一般使用模式(广泛用于框架之中: 获取 - 使用 - 还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 获取
try {
Thread.currentThread().setContextClassLoader(targetLoader);
myMethod();// 使用
} finally {
Thread.currentThread().setContextClassLoader(classLoader);// 最后一定要还原
}
线程类上下文类加载器广泛用于框架开发中。
学习笔记,如有错误,欢迎指正
请问看得第二版还是第三版
第三版