文章目录
- 【JVM系列】深入理解Java虚拟机(JVM)的核心技术:从加载到初始化的全过程解析(一、Java类加载器)
- 1. 类加载器加载的过程
- 2. Class文件读取来源
- 3. 类加载器的分类
- 4. 那些操作会初始化类加载器
- 5. 类加载器的双亲委派机制
- 6. ClassLoader源码解读
- 6.1 Launcher类源码解读
- 6.2 自定义一个类加载器
- 7. 根据类加载器手写热部署插件
- 8. SPI机制
- 9. 如何绕开双亲委派原则
- 10. 常见的几款java虚拟机
【JVM系列】深入理解Java虚拟机(JVM)的核心技术:从加载到初始化的全过程解析(一、Java类加载器)
类加载器(Class Loader)是Java运行时环境中一个重要的组件,它负责在程序运行期间动态地加载类文件到Java虚拟机(JVM)中。类加载器不仅能够从本地文件系统加载类,也可以从网络或者其他自定义的存储位置加载类。这样的设计使得Java程序具有高度的灵活性和扩展性。
1. 类加载器加载的过程
类加载器在Java虚拟机(JVM)中负责加载类文件到内存中,并且进行相应的验证、准备和初始化工作。整个过程可以分为以下几个阶段:
1. 加载(Loading)
在这个阶段,类加载器会根据指定的类全名(包括包名)来查找并加载该类的二进制字节流。这个二进制字节流通常来自于.class
文件,但也可以来自其他地方,例如数据库、网络资源等。加载完成后,类加载器会在内存中生成一个java.lang.Class
对象,表示这个类。
2.验证(Verification)
验证阶段的主要目的是确保读入的字节流包含的信息符合JVM规范的要求,不会危害到虚拟机自身的安全。验证过程包括四个子阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。
- 文件格式验证:确保输入的字节流能正确地解析并符合Class文件格式的规范。
- 元数据验证:对类的方法和字段根据Java语言规范进行语义验证。
- 字节码验证:进行数据流和控制流分析,确保程序不会做出危害虚拟机安全的行为。
- 符号引用验证:确保解析后的类的方法区的常量池里的各种符号引用可以成功地解析成具体的类、接口、字段和方法。
3.准备(Preparation)
准备阶段负责为类的静态变量分配内存,并设置类变量(即static修饰的变量)所需的初始值,这与程序代码无关,而是根据字段的数据类型和是否有显式初始化来决定的。例如所有的基本类型的字段会被初始化为零值(如0、0L、false、null等)。
4.解析(Resolution)
解析阶段是将类的二进制数据中的符号引用替换为直接引用的过程。符号引用就是符号名加上描述符组成的字符串,而直接引用则是可以直接定位到内存地址的指针、相对偏移量或是一个能够间接定位到目标的句柄。
5.初始化(Initialization)
初始化阶段是执行类构造器<clinit>()
方法的过程。<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static
{}块)中的语句合并产生的,编译器收集的顺序按照文本顺序排列。<clinit>()
方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器就可以不为这个类生成<clinit>()
方法。
类的初始化还包括了其父类的初始化,如果类中有静态初始化块,则静态初始化块也会被执行。
需要注意的是,虽然类加载的过程通常按照上述顺序进行,但在某些情况下,例如在某些验证子阶段之间可能会有交叉行为,不是完全线性的。此外,类加载的具体实现细节可能因不同的JVM实现而有所不同。
2. Class文件读取来源
-
本地磁盘文件 java源代码编译的class文件
-
通过网络下载的class文件
-
War、Jar解压的class文件
-
从专门的数据库中读取的class文件
-
使用java cglib、动态代理生成的代理类class文件
3. 类加载器的分类
-
启动(Bootstrap)类加载器:加载JVM自身工作需要的类,它由JVM自己实现。它会加载
$JAVA_HOME/jre/lib
下的文件 底层是C语言实现 -
扩展(Extension)类加载器:它是JVM的一部分,由
sun.misc.LauncherExtClassLoader
实现,他会加载ExtClassLoader
实现,他会加载ExtClassLoader
实现,他会加载JAVA_HOME/jre/lib/ext
目录中的文件(或由System.getProperty(“java.ext.dirs”)
所指定的文件)。 底层是Java实现 -
(应用)AppClassLoader 类加载器:应用类加载器,我们工作中接触最多的也是这个类加载器,它由
sun.misc.Launcher$AppClassLoader
实现。他加载我们工程目录classpath
下的class
及jar
包,底层是java实现 -
自定义类加载器: 也就是用户自己定义的类加载器
import java.util.Arrays;
import java.util.List;
public class JvmDemo01 {
public static void main(String[] args) {
// 应用类加载器
bootstrapClassLoader();
System.out.println("------------------------------");
extClassLoader();
System.out.println("------------------------------");
appClassLoader();
}
/**
* 启动类加载器的职责
*/
public static void bootstrapClassLoader() {
String property = System.getProperty("sun.boot.class.path");
List<String> list = Arrays.asList(property.split(";"));
list.forEach((t) -> {
System.out.println("启动类加载器目录:" + t);
});
}
/**
* 扩展类加载器
*/
public static void extClassLoader() {
String property = System.getProperty("java.ext.dirs");
List<String> list = Arrays.asList(property.split(";"));
list.forEach((t) -> {
System.out.println("扩展类加载器" + t);
});
}
/**
* app 类加载器
*/
public static void appClassLoader() {
String property = System.getProperty("java.class.path");
List<String> list = Arrays.asList(property.split(";"));
list.forEach((t) -> {
System.out.println("应用类加载器" + t);
});
}
}
4. 那些操作会初始化类加载器
类的主动使用:
- 调用类的静态方法
invokeStatic
调用静态方法Main
new
Class.formname
- 子类初始化一定会初始化父类
初始化一个类,那么一定会触发类加载器;但是类加载器加载了该类,但是该类不一定初始化。
5. 类加载器的双亲委派机制
首先在我们类加载器分为四种自定义类加载器、应用类加载器、扩展类加载器、启动类加载器。
当一个类加载器收到请求之后,首先会依次向上查找到最顶层类加载器(启动类加载器),依次向下加载class文件,如果已经加载到class文件,子加载器不会加继续加载该class文件。
**双亲委派机制机制的好处:**目的就是为了防御开发者为定义的类与jdk定义源码类产生冲突问题,保证该类在内存中的唯一性。
6. ClassLoader源码解读
6.1 Launcher类源码解读
public class JvmDemo02 {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.zhaoli.jvm.test01.JvmDemo01");
Object o = aClass.newInstance();
System.out.println(o.getClass().getClassLoader());
}
}
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) { // 确保类加载的线程安全性
// 首先检查该类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) { // 如果没有加载过,则继续加载过程
long t0 = System.nanoTime(); // 记录开始时间,用于性能统计
try {
if (parent != null) { // 如果存在父类加载器,则尝试让父类加载器加载
c = parent.loadClass(name, false);//继续调用本方法
} else { // 否则,此时调用的是启动类加载器,则尝试加载核心类库中的类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException异常,则说明父类加载器未能找到该类
// ClassNotFoundException thrown if class not found from the non-null parent class loader
}
if (c == null) { // 如果此时类仍然未被加载
// 调用findClass方法来寻找该类
long t1 = System.nanoTime(); // 记录调用findClass的时间点
c = findClass(name); // 这里是由当前类加载器自己去查找并加载类
// 当前类加载器是定义类的加载器;记录相关统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) { // 如果需要解析类,则调用resolveClass方法来解析类
resolveClass(c);
}
return c; // 返回加载好的类
}
}
private Class<?> findBootstrapClassOrNull(String name) {
if (!checkName(name)) return null; // 检查类名是否合法,如果不合法则返回null
// 尝试通过启动类加载器来加载类
return findBootstrapClass(name);
}
// 如果找不到指定的类,则返回null
private native Class<?> findBootstrapClass(String name);
}
代码为Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.zhaoli.jvm.test01.JvmDemo01");
-
先进入
loadClass()
- 发现此类没有被加载过
- 存在父类加载器继续调用
loadClass()
让父类加载器加载 - 循环(发现此类没有被加载过)(自动向上依次检查是否已经被加载过)
- 存在父类加载器继续调用
loadClass()
(让父类加载器加载)直到没有父类加载器 - 调用
findBootstrapClassOrNull()
尝试加载核心类库中的类 - 发现没有(
if (c == null)
)则(自定向下依次加载)调用findClass()
(此时是扩展类加载器)查找此类 - 返回上一级的
loadClass()
方法,发现没有(if (c == null)
)则(自定向下依次加载)调用findClass()
(此时是应用类加载器)查找此类 - 最终找到此类(是应用类加载器)找到的
-
当我们将
com.zhaoli.jvm.test01.JvmDemo01
类对应的class类拷贝到D:\Java\jdk1.8\jre\
目录下(注意并不是直接拷贝JvmDemo01.class
而是拷贝整个文件夹target/classes/com/zhaoli/jvm/test01/JvmDemo01.class
)- 此时先进入
loadClass()
- 发现此类没有被加载过
- 存在父类加载器继续调用
loadClass()
让父类加载器加载 - 循环(发现此类没有被加载过)(自动向上依次检查是否已经被加载过)
- 存在父类加载器继续调用
loadClass()
(让父类加载器加载)直到没有父类加载器 - 调用
findBootstrapClassOrNull()
尝试加载核心类库中的类 - 不进入
if (c == null)
(此时是启动类加载器) - 不进入
if (c == null)
(此时是应用类加载器) - 最终找到此类(是启动类加载器)找到的
- 此时先进入
代码为:Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("java.lang.Stirng");
- 先进入
loadClass()
=>发现此类被加载过(Stirng
提前已经加载过了)=>直接返回
6.2 自定义一个类加载器
public class TestClassLoader extends ClassLoader {
private File fileObject;
public TestClassLoader(File fileObject) {
this.fileObject = fileObject;
}
public void setFileObject(File fileObject) {
this.fileObject = fileObject;
}
public File getFileObject() {
return fileObject;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = getClassFileBytes(this.fileObject);
System.out.println("进入到了自定义类加载器(TestClassLoader)的 findClass() 来加载 class 类");
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 从文件中读取去class文件
*/
private byte[] getClassFileBytes(File file) throws Exception {
//采用NIO读取
FileInputStream fis = new FileInputStream(file);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileC.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outC.write(buffer);
buffer.clear();
}
fis.close();
return baos.toByteArray();
}
}
public class JvmDemo03 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
TestClassLoader testClassLoader = new TestClassLoader(new File("D:\\code\\Test01.class"));
Class<?> aClass = testClassLoader.loadClass("com.zhaoli.jvm.test01.Test01");
Object o = aClass.newInstance();
System.out.println(o);
}
}
7. 根据类加载器手写热部署插件
-
如何判断一个
class
文件是否发生变化? MD5或者操作系统提供api 文件修改时间 -
判断该class文件修改日期是否有发生变化,如果有发生变化,则从新使用类加载器读取最新的class文件到内存中。
-
如何监听class文件是否有发生变化呢?单独线程
public class ClassFileEntity {
/**
* 类的名称
*/
private String name;
/**
* class
*/
private Class aClass;
/**
* 最后被更改的时间
*/
private long lastModified;
public ClassFileEntity(String name, long lastModified) {
this.name = name;
this.lastModified = lastModified;
}
public ClassFileEntity(String name, long lastModified, Class aClass) {
this.name = name;
this.lastModified = lastModified;
this.aClass = aClass;
}
//此处省略get()和set()
}
@Slf4j
public class HotDeploymentPlug {
//存放所有的class文件
private Map<String, ClassFileEntity> mapClassFiles = new HashMap<>();
private String path;
/**
* 包的名称
*/
private String packageName = "com.zhaoli.demo.";
public HotDeploymentPlug(String path) {
this.path = path;
}
public void start() {
listener();
}
/**
* 监听方法
*/
public void listener() {
new Thread(() -> {
while (true) {
// 1.读取该文件下
File files = new File(path);
File[] tempList = files.listFiles();
// 2.读取class文件 存入到 mapClassFiles
for (File file : tempList) {
String name = file.getName();
if (StringUtils.isEmpty(name)) {
continue;
}
long l = file.lastModified();
// 使用类加载器读取该 class
String className = packageName + name.replace(".class", "");
if (mapClassFiles.containsKey(className)) {
// 则比对该class文件 是否被修改
ClassFileEntity mapClassFileEntity = mapClassFiles.get(className);
if (mapClassFileEntity.getLastModified() != l) {
try {
mapClassFileEntity.setLastModified(l);
TestClassLoader testClassLoader = new TestClassLoader(file);
Class<?> aClass = testClassLoader.loadClass(className);
Object o = aClass.newInstance();
log.info(className + "class文件发生了变化");
} catch (Exception e) {
log.error("e:{}", e);
}
}
} else {
ClassFileEntity newClassFileEntity = new ClassFileEntity(className, l);
// 如果不存在 则存入到mapClassFiles集合中
mapClassFiles.put(className, newClassFileEntity);
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
public static void main(String[] args) {
HotDeploymentPlug hotDeploymentPlug = new HotDeploymentPlug("D:\\code\\com\\zhaoli\\demo");
hotDeploymentPlug.listener();
}
8. SPI机制
SPI 机制(Service Provider Interface)是 Java 中的一种服务发现机制,用于在运行时动态加载和使用类库中的服务实现。SPI 机制允许开发者定义服务接口,并让不同的服务提供者提供自己的实现,而不必修改原始代码或重新编译。
SPI 机制的基本原理:
-
服务接口定义:开发者首先定义一个服务接口(通常是一个抽象类或接口),这个接口声明了服务应提供的方法。
-
服务实现:第三方开发者可以提供这个接口的具体实现类。这些实现类通常放在项目的类路径下的某个包内。
-
配置文件:实现类的信息记录在一个名为
services
目录下的文本文件中,文件名是服务接口的全限定名(包名+类名)。每个文件包含一行或多行,每行对应一个实现类的全限定名。 -
加载实现类:当应用程序启动时,JVM 会查找并加载这些配置文件,并根据配置文件中的信息实例化相应的服务实现类。
示例:
假设有一个服务接口 com.example.MyService
,并且有两个实现类 com.example.impl.MyServiceImpl1
和 com.example.impl.MyServiceImpl2
。
配置文件内容:
在 src/main/resources/META-INF/services/com.example.MyService
文件中,内容如下:
com.example.impl.MyServiceImpl1
com.example.impl.MyServiceImpl2
加载实现类:
当应用程序需要使用 MyService
接口的服务时,可以通过以下代码加载实现类:
import java.util.ServiceLoader;
public class ServiceProviderDemo {
public static void main(String[] args) {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
System.out.println("Loaded service implementation: " + service.getClass().getName());
// 调用服务实现的方法
service.doSomething();
}
}
}
SPI 机制的优点:
-
扩展性:无需修改源代码即可添加新的服务实现。
-
灵活性:可以在运行时动态加载不同的服务实现。
-
插件式架构:适合构建插件式的架构,允许第三方扩展核心功能。
SPI 机制的局限性:
-
静态加载:SPI 机制在类加载时就会加载所有实现类,无法在运行时动态注册或卸载服务。
-
单一实例:SPI 默认为每个服务提供者创建一个单例对象,如果服务实现依赖于外部状态或其他初始化参数,可能需要额外的处理。
-
线程安全:如果有多个实现类,SPI 机制需要保证线程安全,否则可能会出现并发问题。
9. 如何绕开双亲委派原则
双亲委派模型(Parent Delegation Model)是 Java 类加载器体系结构的核心组成部分。在这个模型中,每个类加载器负责先将其加载请求委托给父类加载器,只有当父类加载器无法完成加载请求时,才会尝试自己加载。这种机制有助于确保类的一致性和避免类加载冲突。
**绕开双亲委派模型的方法:**绕开双亲委派模型通常意味着你需要自定义类加载器,并且在某些情况下直接加载类而不经过标准的委托流程。以下是一些常见的方法:
1. 自定义类加载器
你可以创建一个自定义的类加载器,覆盖 findClass()
方法,并且在 loadClass()
方法中不调用 super.findClass()
,而是直接加载类。这样就可以绕过标准的委托机制。
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
private final String name;
private final byte[] classData;
public CustomClassLoader(String name, byte[] classData) {
this.name = name;
this.classData = classData;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return defineClass(name, classData, 0, classData.length);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals(this.name)) {
return findClass(name);
} else {
return super.loadClass(name); // 这里仍然可以调用父类加载器
}
}
// 其他必要的方法,如getResourceAsStream等
}
2. 使用 defineClass()
方法
你可以使用 ClassLoader
的 defineClass()
方法来直接定义类,而不是通过标准的类加载流程。这种方式通常用于从字节数组中加载类,例如从网络、磁盘或者其他来源获取的字节码数据。
public class CustomClassLoader extends ClassLoader {
public Class<?> defineClass(byte[] classData) {
return defineClass("CustomClassName", classData, 0, classData.length);
}
}
3. 使用 ClassLoader.defineClass(String name, byte[] b)
方法
这种方法直接使用 ClassLoader
类的静态方法来定义类,这种方式不依赖于类加载器的继承关系。
public class CustomClassLoader extends ClassLoader {
public Class<?> defineClass(byte[] classData) {
return defineClass("CustomClassName", classData, 0, classData.length);
}
}
注意事项:
-
安全性:绕过双亲委派模型可能会导致安全性和一致性问题,特别是当涉及到核心类库的加载时。
-
兼容性:如果你的类加载逻辑过于复杂,可能会导致与其他框架或库的兼容性问题。
-
维护性:自定义类加载器增加了系统的复杂性,可能会使代码更难以维护。
10. 常见的几款java虚拟机
Java 虚拟机(JVM)是 Java 运行时环境的核心组件,它负责执行 Java 字节码。不同的 JVM 实现有不同的特性和适用场景。以下是几种常见的 Java 虚拟机:
-
Oracle HotSpot VM:HotSpot 是最常用的 Java 虚拟机,由 Sun Microsystems 开发(后来被 Oracle 收购)。HotSpot VM 是 OpenJDK 和 Oracle JDK 的默认 JVM,它具有以下特点:
-
性能优化:HotSpot 包含了许多性能优化技术,如即时编译(JIT)、垃圾回收(GC)算法等。
-
开源:HotSpot 是 OpenJDK 项目的一部分,因此它是开源的。
-
广泛支持:几乎所有的现代 Java 开发环境都支持 HotSpot。
-
-
OpenJ9:OpenJ9 是 IBM 开发的一款高性能的 Java 虚拟机,现在由 Eclipse 基金会维护。OpenJ9 作为 IBM 的 Java SE 8 和 Java SE 11 发布版的基础,并且也被用于 Adoptium(之前称为 AdoptOpenJDK)项目中。OpenJ9 的特点包括:
-
内存效率:OpenJ9 通常具有较低的内存使用。
-
性能:在某些工作负载下,OpenJ9 表现出色,尤其是那些需要长时间运行的应用程序。
-
可移植性:OpenJ9 支持多种操作系统和硬件平台。
-
-
GraalVM:GraalVM 是 Oracle 开发的一款多语言虚拟机,支持 Java、JavaScript、Python、Ruby 等多种语言。GraalVM 的特点是:
-
多语言支持:除了 Java,还支持其他动态语言。
-
AOT 编译:支持提前编译(Ahead-of-Time Compilation),可以将 Java 字节码转换为本地代码,提高启动速度和运行时性能。
-
内存效率:GraalVM 通常具有较好的内存效率。
-
-
Zing:Zing 是 Azul Systems 开发的一款企业级 Java 虚拟机,专为大规模、长时间运行的应用程序设计。Zing 的特点包括:
-
Zulu:Zulu 是 Azul Systems 提供的一款开源 Java 虚拟机实现,基于 OpenJDK。Zulu 支持多种 Java 版本,并且是完全免费和开源的。