JVM Class Loader SubSystem
概述
- Java虚拟机中执行的指令称为字节码指令, 类加载器(Class Loader)是负责将字节码(.class)文件按二进制流的方式装载到方法区的, 然后通过执行引擎(Execution Engine)来解释/ 编译为对应平台的机器指令后执行的
- 类加载器只负责将字节码文件装载到内存, 至于是否可以运行, 由执行引擎来判断
类加载的过程
加载阶段(Loading)
- 从 .class文件读取字节码按二进制流的方式装载到方法区
- 在内存中生成指定类的 java.lang.Class实例对象, 作为此类访问入口
链接阶段(Linking)
- 验证(Linking)
- 主要包括4种验证: 文件格式(如魔数, 版本号等), DNA元数据(如字节码进行语义分析, 检查是否符合 Java语言规范), 字节码(如程序语义合法性, 逻辑等), 符号引用
- 准备(Prepare)
- 为类变量分配内存, 并设置(指定数据类型的)默认初始值, 但不包含 final修饰的 static(因为通过 final修饰后是会在编译的时候已经分配, 然后在此准备阶段时会显式的初始化)
- 加载器的准备阶段是在方法区进行的, 而不同与类实例对象, 创建实例对象时的内存分配是在堆中进行的
- 解析(Resolve)
- 主要解析类, 方法, 方法类型, 字段, 接口, 接口方法等. 同时将常量池中的符号引用转换为直接引用
初始化阶段(Initialization)
- 类中有静态变量或静态代码块时, 会在对应类的字节码文件中, 产生针对静态变量的构造器<clinit>(), 如果没有静态变量就不会产生<clinit>(). 在此阶段给每个静态变量显式的赋值 注: 变量赋值顺序是 Java源代码中的代码顺序
public class App {
static {
age = 10;
}
private static Integer age = 35;
public static void main(String[] args) {
System.out.println(age);
}
}
# 输出: 35
- 若类有父类, 父类的<clinit>(), 会优先执行后, 执行子类的构造器
- 虚拟机执行类加载时, 只会调用一次<clinit>(), 所以多线程下是有同步加锁的
public class App {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
Demo demo = new Demo();
System.out.println(Thread.currentThread().getName() + "结束");
};
new Thread(r, "线程01").start();
new Thread(r, "线程02").start();
}
}
class Demo {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "首次初始化!");
while (true) {}
}
}
}
# 输出:
线程01开始
线程02开始
线程01首次初始化! # 验证在此会被锁住
- JVM虚拟机规范上, 类加载器有两种分别为引导类加载器和自定义加载器. 规范上(派生于 ClassLoader类的加载器都划分为自定义类加载器)
3种类加载器& 自定义加载器
- 启动/引导类加载器(BootStrap ClassLoader)
- 此加载器是使用 C/C++编写的
- 没有父级加载器(不继承 ClassLoader类)
- 主要用于加载核心类库 如: %JRE_HOME%\lib\rt.jar, resources.jar, charsets.jar等
- 还有 sun.boot.class.path路径下的内容用于加载JVM自身需要用的类
- 出于安全原因, 引导类加载器只加载包名为 java, javax, sun开头的类
- 扩展类加载器(Extension ClassLoader)
- 此加载器是使用 Java编写的, 类位置在 %JRE_HOME%\lib\rt.jar!\sun\misc\Launcher.class, 是 sun.misc.Launcher.class内的静态内部类 ExtClassLoader. (sun.misc.Launcher是虚拟机的入口)
- 派生于 ClassLoader类
- 自动加载扩展目录内的 jar包 %JRE_HOME%\lib\ext, 和通过选项 -Djava.ext.dirs=指定的目录
- 应用/系统类加载器(Application ClassLoader)
- 此加载器是使用 Java编写的, 类位置在 %JRE_HOME%\lib\rt.jar!\sun\misc\Launcher.class, 是 sun.misc.Launcher.class内的静态内部类 AppClassLoader
- 派生于 ClassLoader类
- 自动加载环境变量 classpath内属性 java.class.path指定路径下的类库
- 此加载器是默认类加载器, 自己编写的类默认由它来完成加载的
try {
System.out.println(
Class.forName("org.example.Test").getClassLoader()
);
} catch (Exception e) {}
# 输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
public class ClassLoaderApp {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
# 输出:
sun.misc.Launcher$AppClassLoader@18b4aac2 默认应用类加载器
sun.misc.Launcher$ExtClassLoader@610455d6 之上级是扩展类加载器
null 由于引导类加载器是 native通过C语言编写的, 所以只返回 null
获取 ClassLoader的4种方式
- ClassLoader类是抽象类, 除启动类加载器外, 其它加载器都继承自它
1.获取类的 Class.forName(“java.lang.String”).getClassLoader()
2.获取当前线程上下文的 Thead.currentThread().getContextClassLoader()
3.获取系统的 ClassLoader.getSystemClassLoader()
4.获取调用者的 DriverManager.getCallerClassLoader()
自定义类加载器(User Defined ClassLoader)
- 自定义类加载器的使用场景
- 隔离加载类: 当使用中间件时, 为了防止与项目内其它框架包冲突
- 修改类加载的方式: 除引导加载器以外的加载器, 可以按需要动态加载
- 扩展加载源: 加载方式可以多元化 如 通过网络, 压缩包, 数据库等
- 防止源码泄漏: 为了防止反编译, 可以将字节码文件通过自定义加载器做加解密
- 实现自定义类加载器步骤
- 继承抽象类 java.lang.ClassLoader
- 在 JDK1.2前继承 ClassLoader类后, 需重写 loadClass()方法, 但之后版本已不再建议重写 loadClass(), 而是建议将自定义类加载逻辑写在 findClass()方法中
- 所要编写的自定义类加载器, 如果不复杂(没有加解密等需求)可以直接继承 URLClassLoader类(ClassLoader的子类). 这样可以避免自己写 findClass()方法及其获取字节码流的方式
- 加载类的2种方式
- 通过 Class.forName()方法动态加载, 默认初始化静态变量, 静态变量的初始化也可以可选 Class.forName(name, initialize, loader)中, 通过第二个参数 initialze控制
- 通过 ClassLoader.loadClass()方法动态加载, 不初始化静态变量
public class ClassLoaderApp extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 指定字节码文件路径通过二进制方式读取, 如果字节码文件有锁, 则在此处进行解密
return null;
}
public static void main(String[] args) {
ClassLoaderApp loader = new ClassLoaderApp();
try {
Class<?> _class = Class.forName("org.example.Test2", true, loader);
Test test = (Test) _class.newInstance();
System.out.println(test.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
双亲委派机制
- 工作原理:
- 如果一个类加载器收到加载请求, 首先委托给父类加载器去执行
- 如父类还有父类加载器, 则进一步向上委托, 依次递归请求, 最终在顶层启动类加载器
- 如父类加载器可以完成加载就成功返回, 若父类无法完成加载, 子加载器才会尝试自己去加载, 这就是双亲委派模型
- 最后只会被一个加载器所加载
双亲委派机制优势
- 避免类的重复加载
- 防止核心 API被篡改(Java核心源代码保护, 又称为沙箱安全机制)
# 例: 创建包名和名称相同的 String类
package java.lang;
public class String {
static {
System.out.println("初始化自定义 String类");
}
public static void main(String[] args) {
System.out.println("自定义的 String.main()");
}
}
# 输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
- 通过双亲委派机制, 首先会从顶层的引导类加载器(不过包名开头为 java时默认会选择引导类加载器), 尝试查找指定类(包名和名称相同的类), 同时默认核心类库中的 String类内没有 main方法, 因此找不到 main方法
public class ClassLoaderApp {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
}
}
# 输出:
无输出
- 由于在引导类加载器, 已经加载过了核心类库中的 java.lang.String, 因此自定义 String类未被加载
其它
- JVM中表示两个对象是否为同一个类的, 判断条件是类名称和包路径相同, 同时加载器的实例也必须相等
- JVM中记录一些类型信息时, 也会将类加载器的一个引用也作为类型的一部分存在方法区中
类有2种使用方式
- 分别为主动使用和被动使用, 区别是首次主动使用时会初始化静态变量及静态块, 而被动使用则不会
被动使用例子:
(1) 通过子类引用访问父类的静态变量, 属子类的被动使用, 不会导致子类的初始化
class A {
static int count = 1;
static {
System.out.println("初始化 A类");
}
}
class B extends A {
static {
System.out.println("初始化 B类");
}
}
public class App {
public static void main(String[] args) {
System.out.println(B.count);
}
}
# 输出:
初始化 A类
1
(2) 在编译阶段, 会将常量存入到, 调用方法所在的类的常量池中, 所以没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化
class D {
static final int count = 1;
static {
System.out.println("初始化 D类");
}
}
public class ClassLoaderApp {
public static void main(String[] args) {
System.out.println(D.count);
}
}
# 输出:
1
# 但是, 如果将 D类的变量改为以下形式, 就属于主动使用, 所以 D类会被初始化. 因为 UUID.randomUUID().toString()方法是运行期确认的
class D {
static final String uuid = UUID.randomUUID().toString();
static {
System.out.println("初始化 D类");
}
}
public class ClassLoaderApp {
public static void main(String[] args) {
System.out.println(D.uuid);
}
}
# 输出:
初始化 D类
ac65e308-ea8f-4734-bb72-e29a013954a0
7种主动使用的方式
- 通过 new关键字创建类实例
- 访问指定类的静态变量
- 调用指定类的静态方法
- 通过反射加载类 例 Class.forName(“org.example.Test”)
- 初始化指定类的子类
- 虚拟机启动时被表明为启动类的类
- jdk7开始提供的动态语言支持: java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic, REF_putStatic, REF_invokeStatic句柄对应类没有初始化, 则初始化
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!