JVM 类加载机制
JVM类加载机制是Java运行时环境的核心部分,它负责将类的.class文件加载到JVM中,并将其转换为可以被JVM执行的数据结构。
类加载的整体流程
类加载的整体流程可以分为五个阶段:加载(Loading)、链接(Linking)、初始化(Initialization)、使用和卸载(Unloading)。其中,链接阶段又可以细分为验证(Verification)、准备(Preparation)和解析(Resolution)三个阶段。
加载阶段
- 系统提供的类加载器或自定义类加载器:首先,通过类加载器(可以是系统提供的,也可以是用户自定义的)根据类的全名(包括包名)找到对应的.class文件。
- 读取.class文件:类加载器从文件系统或网络等位置读取.class文件的二进制数据。
- 创建Class对象:将读取到的二进制数据转换为方法区的运行时数据结构,并在堆区创建一个java.lang.Class对象,这个Class对象作为该类在JVM中的元数据表示。
链接阶段
- 验证阶段:
- 文件格式验证:验证.class文件是否符合JVM规范,是否是一个有效的字节码文件。
- 元数据验证:验证字节码中的元数据是否符合Java语言规范。
- 字节码验证:验证字节码的执行是否符合Java虚拟机规范。
- 符号引用验证:验证类中的符号引用是否有效,能否被正确解析。
- 准备阶段:为类的静态变量分配内存空间,并设置初始值(注意,这里的初始值不是代码中显式赋予的值,而是根据变量的数据类型赋予的默认值,如int为0,引用类型为null)。
- 解析阶段:将常量池中的符号引用转换为直接引用。符号引用是一个抽象的概念,如字段名、方法名等,而直接引用则是指向内存中的具体地址。
初始化阶段
- 初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有变量的赋值动作和静态代码块(static代码块)中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先初始初始化其父类。
- JVM会保证一个类的()方法在多线程环境中被正确的加锁和同步。因此JVM中一个类的class是线程安全的。
- 只有当类或接口的静态变量被首次主动使用时,JVM才会初始化这个类或接口。
类的主动引用(一定会发生类的初始化)
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当JVM启动,java Hello,则一定会初始化Hello类(即先启动main方法说在的类)
- 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类
注意:
一个类被初始化后,不会重复进行被初始化
类的被动引用(不会发生类的初始化)
- 当访问一个静态属性是,只有真正声明这个域的类才会被初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(因为常量在编译节点就存入调用类的常量池中了)
使用和卸载阶段
- 类被初始化后,就可以被JVM使用,创建实例对象、调用方法等。
- 当类不再被使用时,JVM会将其从内存中卸载,释放其占用的资源。
测试类的加载机制
创建一个A对象和一个B对象,使B对象继承A,测试时初始化A,查看JVM类的加载过程
A.java
package demo;
public class A {
/** 定义一个静态属性 */
public static String NAME = "A";
/** 定义一个final 静态常量 */
public static final int AGE = 20;
static {
System.out.println("A static 代码块被调用了");
NAME = "static 修改了 NAME";
}
public A(){
System.out.println("A 默认构造方法被调用了");
}
}
B.java
package demo;
public class B extends A {
/** 定义一个静态属性 */
private static String TYPE = "A";
/** 定义一个final 静态常量 */
private static final int WIDTH = 20;
static {
System.out.println("B static 代码块被调用了");
TYPE = "static 修改了 TYPE";
}
public B(){
System.out.println("B 默认构造方法被调用了");
}
}
ClassLoadDemo.java
package demo;
/**
* 测试JVM类的加载机制
*
* @author Anna.
* @date 2024/4/5 14:14
*/
public class ClassLoadDemo {
static {
System.out.println("mian方法所在类的 static 代码块被调用了");
}
public static void main(String[] args) {
new B();
System.out.println("初始化完成后-第二次调用不会重复进行初始化");
new B();
}
}
执行结果:
结论:
一个类初始化时,如果其父类没有被初始化,则先初始化其父类。
一个类被初始化后,再次被引用时,则不会重复初始化。
new 对象会默认调用类的无参构造方法
测试类的主动引用,一定会发生类的初始化
类定义使用上述A.java及B.java
- 通过new一个对象
public class ClassLoadDemo1 {
public static void main(String[] args) {
System.out.println("========1 通过new========");
new A();
}
}
执行结果:
- 调用类的静态成员(除了final常量)和静态方法
package demo;
public class ClassLoadDemo1 {
public static void main(String[] args) {
System.out.println("========2 调用类的静态成员(除了final常量)和静态方法========");
System.out.println("调用final常量:");
System.out.println("A.AGE:" + A.AGE);
System.out.println("调用非final静态成员常量:");
System.out.println("A.NAME:" + A.NAME);
}
}
执行结果:
- 使用java.lang.reflect包的方法对类进行反射调用
package demo;
public class ClassLoadDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("========3 反射调用========");
Class.forName("demo.A");
}
}
执行结果:
类的被动引用,不会发生类的初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
package demo;
public class ClassLoadDemo2 {
public static void main(String[] args) {
System.out.println("========通过子类引用父类的静态变量,不会导致子类初始化========");
System.out.println("========子类引用父类final常量,既不初始化父类,也不初始化子类========");
System.out.println("B.AGE" + B.AGE);
System.out.println("========子类引用父类非final常量,初始化父类,但不初始化子类========");
System.out.println("B.NAME" + B.NAME);
}
}
执行结果:
- 通过数组定义引用,不会触发此类的初始化
package demo;
public class ClassLoadDemo2 {
public static void main(String[] args) {
System.out.println("========2 通过数组定义引用,不会触发此类的初始化========");
A[] arr = new A[10];
}
}
执行结果:
类加载器
- Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的重要组成部分,负责动态加载Java类到Java虚拟机的内存空间中。
- 类加载器在Java程序中扮演着至关重要的角色,它确保了类的正确加载、链接和初始化,为程序的执行提供了基础。
类加载器的层次结构
Java类加载器的层次结构是一个有序的组织形式,它定义了类加载器之间的父子关系和加载范围,确保了Java程序的正确运行。
Java类加载器的层次结构通常包括四种主要类型的类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器。这些类加载器按照父子关系组织,形成了一个有序的加载链。
- 启动类加载器(Bootstrap ClassLoader):
- 是Java虚拟机的一部分,通常由本地代码实现(C语言),负责加载Java核心类库,如java.lang包中的类。
- 由于是由本地代码实现的,因此并不基础自java.lang.classLoader。在Java代码中无法直接获取其引用。
- 负责加载JVM运行时环境所需的基础类库,是所有类加载器的根加载器。
- 扩展类加载器(Extension ClassLoader):
- 是由Java语言实现的,用于加载Java扩展类库,如javax包中的类。
- 它的父加载器是启动类加载器(但无法通过java获取其父类)。
- 可以通过系统属性"java.ext.dirs"来指定扩展类库的路径。
- 应用程序类加载器(Application ClassLoader):
- 也称为系统类加载器,是默认的类加载器,负责加载应用程序的类路径(classpath,java.lang.path)下的类文件。
- 它的父加载器是扩展类加载器。
- 可以通过ClassLoader类的getSystemClassLoader()方法获取到它的引用。
- 有sum.misc.Launcher$AppClassLoader实现
- 自定义类加载器
- 开发者可以通过基础java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊需求。
层次结构的优势
- 安全性:通过双亲委派机制,确保了Java核心类库的安全性,防止了恶意代码通过自定义类加载器来篡改或替换核心类库。
- 有序性:层次结构确保了类的加载是有序的,避免了类的重复加载。
- 灵活性:通过自定义类加载器,可以实现热加载、隔离加载环境、加载加密类文件等高级功能。
ClassLoader介绍
作用
- java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对于的字节代码,然后从这些字节代码中定义出第一个Java类,即java.lang.Class类的一个实例。
相关方法
方法 | 描述 |
---|---|
loadClass(String name) | 这是 ClassLoader 的核心方法,用于加载指定的类。当应用程序请求加载一个类时,ClassLoader 首先会检查这个类是否已经被加载过。如果已经加载过,就直接返回该类的 Class 对象。否则,会尝试加载该类。 如果 ClassLoader 自己无法找到类,它会根据双亲委派模型将请求委托给父类加载器。如果父类加载器也无法加载类,那么 ClassLoader 会尝试自己加载类。 |
findClass(String name) | 这是一个受保护的方法,用于查找指定名称的类。当 ClassLoader 需要自己加载类时,它会调用该方法。具体的类加载逻辑通常会在该方法中实现。 在自定义 ClassLoader 时,开发人员可以重写该方法以实现自己的类加载逻辑。 |
findLoadedClass(String name) | 该方法用于查找已经由当前 ClassLoader 加载的类。如果该类已经被加载,则返回对应的 Class 对象;否则返回 null。 |
findSystemClass(String name) | 该方法用于查找由系统 ClassLoader(通常是 AppClassLoader)加载的类。它不会委托给父类加载器,而是直接在当前 ClassLoader 或系统 ClassLoader 中查找类。 |
defineClass(String name, byte[] b, int off, int len) | 该方法用于将字节码数组转换为 Class 对象。它允许从字节码数组直接定义类,而不需要从文件系统或网络加载类文件。 在某些高级场景中,如动态代理或代码生成,开发人员可能会使用该方法动态地创建类。 |
getResource(String name) 和 getResources(String name) | 这两个方法用于查找资源(如文件、图像等)。它们根据类加载器的类路径查找资源,并返回 URL 对象或 URL 对象的枚举。 |
getParent() | 该方法返回当前 ClassLoader 的父类加载器。通过该方法,可以访问双亲委派模型中的父级加载器。 |
案例
package demo2;
public class ClassLoadDemo {
public static void main(String[] args) {
System.out.printf("获取当前应用程序类加载器:%s%n", ClassLoader.getSystemClassLoader());
System.out.printf("获取应用程序类加载器父类加载器:%s%n", ClassLoader.getSystemClassLoader().getParent());
System.out.printf("获取根加载器:%s%n", ClassLoader.getSystemClassLoader().getParent().getParent());
System.out.printf("获取应用类路径:%s%n", System.getProperty("java.class.path"));
}
}
执行结果:
类加载器的代理模式
- 代理模式
- 交给其他加载器来加载指定的类
- 双亲委派机制
- 在代理模式下,当一个类加载器收到类加载请求时,它首先会检查这个类是否已经被加载过。如果已经加载过,就直接返回这个类的Class对象。如果没有加载过,它会将这个请求委派给父类加载器去完成。这种机制称为双亲委派模型。通过逐级向上委派,最终会到达顶层的启动类加载器。如果启动类加载器无法加载该类,那么请求会逐级向下传递,直到找到能够加载该类的类加载器为止。
- 双亲委派机制是为了保证java核心库的类型安全。这种机制保证了不会出现用户自己定义java.lang.Object类的情况
- 类加载器除了用于加载类,也是安全的最基本的屏障
- 双亲委派机制是代理模式的一种
- 并不是所有的类加载器都采用双亲委派机制
- tomcat服务器加载器也使用代理模式,所不同的是它是首先尝试去加载这个类,如果找不到在代理给父类加载器。
自定义类加载器
自定义类加载器流程:
- 继承:java.lang.ClassLoader
- 首先检查请求的类型是否已经被这个加载装载到了命名空间,如果已加载,则直接返回
- 委派类加载器请求给父类加载器,如果父类加载器能够完成加载,则直接返回加载器加载的Class实例
- 调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),
- 测试案例调用:loadClass()加载class
UserDo.java
package demo3;
/**
* UserDo 实体
* @author Anna.
* @date 2024/4/5 16:31
*/
public class UserDo1 {
private String name;
public UserDo1(String name) {
this.name = name;
}
@Override
public String toString() {
return "UserDo{" +
"name='" + name + '\'' +
'}';
}
}
注意: 该类编译完成后.class文件不要放在应用目录下。JVM双亲委派机制会先使用应用类加载器进行加载,这样会导致无法测试自定义类加载器
CustomClassLoader.java
package demo3;
import java.io.*;
/**
* 自定义类加载器
*
* 首先检查请求的类型是否已经被这个加载装载到了命名空间,如果已加载,则直接返回
* + 委派类加载器请求给父类加载器,如果父类加载器能够完成加载,则直接返回加载器加载的Class实例
* + 调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),loadClass()转抛异常,终止加载过程
* @author Anna.
* @date 2024/4/5 16:07
*/
public class CustomClassLoader extends ClassLoader{
/** 定义一个加载根路径 */
private String rootDir;
public CustomClassLoader(String rootDir){
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载该类是否已经加载到命名空间
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 尝试自己加载类
byte[] classData;
try {
classData = getClassData(name);
} catch (IOException e) {
throw new ClassNotFoundException();
}
// 如果自己也加载不到该类,则抛出异常
if(classData == null){
throw new ClassNotFoundException();
}
// 调用defineClass()导入类型到方法区
clazz = defineClass(name, classData, 0, classData.length);
return clazz;
}
/**
* 根据路径读取.class文件
*
* @param name
* @return byte[]
* @author Anna.
* @date 2024/4/5 16:24
*/
private byte[] getClassData(String name) throws IOException {
// 将包路径转换为类路径
String path = rootDir + File.separator + name.replace(".", File.separator) + ".class";
// 使用字节流读取class文件
InputStream is = null;
ByteArrayOutputStream baos = null;
try{
is = new FileInputStream(path);
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int temp = -1;
while ((temp = is.read(buffer)) != -1){
baos.write(buffer,0,temp);
}
}
catch (Exception e){
throw e;
}
finally {
if(is != null){
is.close();
}
if(baos != null){
baos.close();
}
}
return baos.toByteArray();
}
}
ClassLoaderDemo.java
package demo3;
/**
* 自定义类加载器 测试
*
* @author Anna.
* @date 2024/4/5 16:33
*/
public class ClassLoaderDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 获取根路径
String path = "D:";
System.out.println(path);
// 获取自定义类加载器
CustomClassLoader customClassLoader = new CustomClassLoader(path);
// 使用同一个类加载器加载UserDo
Class<?> clazz1 = customClassLoader.loadClass("demo3.UserDo");
Class<?> clazz2 = customClassLoader.loadClass("demo3.UserDo");
System.out.printf("clazz1 hashCode:%s%n", clazz1.hashCode());
System.out.printf("clazz2 hashCode:%s%n", clazz2.hashCode());
System.out.printf("判断同一个类加载器加载同一个对象,class是否相同:%s%n", clazz1 == clazz2);
System.out.printf("获取clazz1的类加载器:%s%n", clazz1.getClassLoader());
// 重新创建一个自定义类加载器
CustomClassLoader customClassLoader2 = new CustomClassLoader(path);
// 使用不同类加载器加载UserDo
Class<?> clazz3 = customClassLoader2.loadClass("demo3.UserDo");
System.out.printf("clazz3 hashCode:%s%n", clazz3.hashCode());
System.out.printf("判断不同类加载器加载同一个对象,class是否相同:%s%n", clazz1 == clazz3);
System.out.printf("获取clazz3的类加载器:%s%n", clazz1.getClassLoader());
}
}
执行结果:
注意:被两个类加载器加载同一个类,JVM不认为是相同的类。
加密解密类加载器
可以通过取反操作或者DES对称秘钥进行加密解密
EncrptUtils.java
package demo4;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 取反加密工具
*
* @author Anna.
* @date 2024/4/5 17:36
*/
public class EncrptUtils {
/**
* 文件字节取反加密
*
* @param src 原路径
* @param dest 目标路径
* @return void
* @author Anna.
* @date 2024/4/5 17:39
*/
public static void inversion(File src, File dest) throws IOException {
try (FileInputStream is = new FileInputStream(src); FileOutputStream baos = new FileOutputStream(dest)) {
int temp = -1;
while ((temp = is.read()) != -1) {
// 取反
baos.write(temp ^ 0xff);
}
} catch (Exception e) {
throw e;
}
}
}
DecrptClassLoader.java
package demo4;
import java.io.*;
/**
* 自定义解密类加载器
*
* @author Anna.
* @date 2024/4/5 16:07
*/
public class DecrptClassLoader extends ClassLoader {
/**
* 定义一个加载根路径
*/
private String rootDir;
public DecrptClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载该类是否已经加载到命名空间
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 尝试自己加载类
byte[] classData;
try {
classData = getClassData(name);
} catch (IOException e) {
throw new ClassNotFoundException();
}
// 如果自己也加载不到该类,则抛出异常
if (classData == null) {
throw new ClassNotFoundException();
}
// 调用defineClass()导入类型到方法区
clazz = defineClass(name, classData, 0, classData.length);
return clazz;
}
/**
* 根据路径读取.class文件
*
* @param name
* @return byte[]
* @author Anna.
* @date 2024/4/5 16:24
*/
private byte[] getClassData(String name) throws IOException {
// 将包路径转换为类路径
String path = rootDir + File.separator + name.replace(".", File.separator) + ".class";
// try-with-resources
try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
// 使用字节流读取class文件 字节取反
int temp = -1;
while ((temp = is.read()) != -1) {
// 字节取反
baos.write(temp ^ 0xff);
}
return baos.toByteArray();
}
}
}
ClassLoadDemo.java
package demo4;
import java.io.File;
import java.io.IOException;
/**
* 自定义解密类加载器
* * 1 加密UserDo.class文件
* * 2 使用上一案例中类加载器加载加密后的class
* * 3 使用解密类加载器加载
*
* @author Anna.
* @date 2024/4/5 17:36
*/
public class ClassLoadDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1 加密.class文件
EncrptUtils.inversion(new File("D://demo3/UserDo.class"), new File("D://temp/demo3/UserDo.class"));
// 获取根路径
String path = "D:/temp";
// 2 使用上一案例中类加载器加载加密后的class
// 获取自定义类加载器 报错ClassNotFoundException
// CustomClassLoader customClassLoader = new CustomClassLoader(path);
// Class<?> clazz1 = customClassLoader.loadClass("demo3.UserDo");
// 3 使用解密类加载器加载
DecrptClassLoader decrptClassLoader = new DecrptClassLoader(path);
Class<?> clazz2 = decrptClassLoader.loadClass("demo3.UserDo");
System.out.printf("获取clazz3的类加载器:%s%n", clazz2.getClassLoader());
}
}
执行结果:
gitee源码
git clone https://gitee.com/dchh/JavaStudyWorkSpaces.git