本次目标
- 理解
JVM
是什么 - 能够描述
JVM
架构图 - 掌握
JVM
类加载器相关概念
JVM概念
JVM
是Java
虚拟机的简称,是Java
语言的核心组件,负责充当Java
代码和底层操作系统之间的中间层。JVM
的主要任务是将Java
字节码转换为特定平台的机器码并执行。
JVM架构图
JVM
采用了一种基于栈的架构,包括类加载器、运行时数据区、执行引擎和本地方法栈等组件。这些组件协同工作,实现了Java
程序的运行和管理。
JVM类加载器
类加载器的概念
类加载器Class Loader
是Java运行时环境中的一个重要组件,负责加载Java
类和资源文件。它的主要功能是根据类的全限定名查找并加载对应的字节码,将其转换为可执行的 Java
类 。类加载器在Java
虚拟机JVM
中起着至关重要的作用,确保Java
应用程序能够正确地加载和运行类。
类加载器的分类
- 启动类加载器
Bootstrap ClassLoader
:负责加载Java
核心库(如java.lang
包)中的类。它主要加载JAVA_HOME\lib\rt.jar
目录下的类库,或者通过-Xbootclasspath
参数指定的路径中的类库。启动类加载器是用原生代码实现的,位于Java虚拟机的最顶层,没有父类加载器。 - 扩展类加载器
Extensions ClassLoader
:负责加载Java
扩展库(如javax
包)中的类。扩展类加载器继承自启动类加载器,因此它的搜索路径包括Java
核心库和所有已安装的扩展库。扩展类加载器加载JAVA_HOME\lib\ext
目录下的类库,或者通过java.ext.dirs
系统变量指定的路径中的类库。 - 应用程序类加载器
Application ClassLoader
:负责加载用户路径classpath上的类
。应用程序类加载器根据Java
应用程序的类路径classpath
加载类,搜索路径包括Java
应用程序的类路径以及Java
核心库和扩展库。 - 自定义类加载器
User ClassLoader
:负责加载应用之外的类文件。
类加载器的特点
双亲委派机制
什么是双亲委派
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载时,才会尝试执行加载任务
为什么需要双亲委派
- 避免类的重复加载:在Java虚拟机中,每个类加载器都有自己的命名空间,负责加载并维护一组类的定义。当一个类加载器收到加载类的请求时,它会先检查自己的命名空间中是否已经加载此类,如果已经加载,则直接返回已加载的类定义,避免了重复加载。如果没有加载,则将加载请求委派给父加载器,由父加载器继续尝试加载,这样一级一级向上委派,直到找到能够加载该类的加载器或者到达顶层的启动类加载器。这种机制确保了类只会被加载一次。避免了被重复加载的问题。
- 保证类的安全性 :由于父加载器会在委派给子加载器加载类之前尝试加载,意味着核心
Java
类库会由顶层的启动类加载器加载,而不会被其他加载器替换。可以防止恶意代码通过替换核心类库来执行一些危险操作,确保了Java
类库的完整性和安全性。
双亲委派机制源码
scss
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先检查请求的类是否被加载过
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 thrown if class not found
// from the non-null parent class loader
//如果父-类加载器抛出异常,说明父-类加载无法完成加载工作
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//在父-类加载器无法完成加载的时候,再调用本身的findClass方法来进行类加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么需要破坏双亲委派
在实际应用中,双亲委派机制解决了Java
基础类统一加载的问题,但是存在缺陷。JDK
基础类作为典型的API
被用户调用,但是也存在基础类调用用户代码的情况:
比如在使用JDBC
时, 利用DriverManager.getConnection
获取连接时,DriverManager
是由根类加载器Bootstrap
加载的,在加载DriverManager
时,会执行其静态方法,加载初始驱动程序,也就是Driver
接口的实现类;但是这些实现类基本都是第三方厂商提供的,根据双亲委派原则,第三方的类不可能被根类加载器加载。
如何破坏双亲委派
- 继承
ClassLoader
抽象类,重写loadClass
方法,在这个方法可以自定义要加载的类使用的类加载器 - 使用线程上下文加载器,可以通过
java.lang.Thread
类的setContextClassLoader()
方法来设置当前类使用的类加载器类型
JDBC打破双亲委派源码
TODO
缓存机制
JVM中的类加载器通常会使用缓存来存储已加载的类信息,如果某个类已经被其父类加载器加载过,那么子类加载器就不会再次加载该类,直接使用缓存中的类。
类加载器的工作原理
类加载的时机
- 主动引用时加载:当程序使用到某个类时,如果该类还未被加载、链接和初始化,则会触发类的加载过程。主动引用包括创建类的实例、访问类的静态变量、调用类的静态方法等操作。
- 被动引用时加载:当访问某个类的静态常量时,只有声明该常量的类会被加载,而不会触发该类的初始化。被动引用的几种情况包括访问类的静态常量、通过数组定义类的引用类型、引用类的子类等。
- 反射调用时加载 :使用
Java
反射机制来动态创建类的实例、访问类的成员等操作时,会触发类的加载。
类加载的生命周期
- 加载阶段:加载阶段是类加载过程的第一步,它负责查找类的字节码,并将其加载到内存中。在加载阶段,会执行以下操作:
-
- 通过类的全限定名获取类的字节码文件。
- 将字节码文件加载到内存中,并在方法区创建一个代表该类的
Class
对象。 - 解析类的字节码,生成虚拟机可以使用的数据结构。
- 链接阶段:链接阶段是类加载过程的第二步,它负责将已经加载的类与其他类和符号引用进行关联。链接阶段又可以细分为以下几个步骤:
-
- 验证:验证阶段确保加载的字节码符合
Java
虚拟机规范,包括检查字节码的结构、语义和安全性等。 - 准备:准备阶段为类的静态变量分配内存,并设置默认初始值。
- 解析:解析阶段将符号引用转换为直接引用,即将类、方法、字段等的符号引用解析为内存中的直接指针。
- 验证:验证阶段确保加载的字节码符合
- 初始化阶段:初始化阶段是类加载过程的最后一步,它负责执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。初始化阶段会按照程序的执行顺序依次执行,且只会执行一次。
综上所述,类加载的过程可以归纳为加载、链接和初始化三个阶段。其中,加载阶段将类的字节码加载到内存中,链接阶段将类与其他类和符号引用关联,初始化阶段执行类的初始化代码。这三个阶段的顺序是依次进行的,加载阶段首先发生,然后是链接阶段,最后是初始化阶段。
自定义类加载器
目标:自定义类加载器,加载指定路径在D盘下的lib文件夹下的类
步骤:
- 新建一个类
Test.java
typescript
package com.sn.knit.classloader;
public class Test {
public static void main(String[] args) {
}
public void say() {
System.out.println("Hello World");
}
}
- 编译
Test.java
到指定lib
目录 - 自定义类加载器
UserClassLoader
继承ClassLoader
:重写findClass()
和defineClass()
方法
java
package com.sn.knit.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class UserClassLoader extends ClassLoader {
private String classpath;
public UserClassLoader(String classpath) {
super(ClassLoader.getSystemClassLoader());
this.classpath = classpath;
}
/**
* 加载class文件
*
* @param className 类的全限名称,定位class文件
* @return {@link Class}<{@link ?}> Class对象
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// 输入流,通过类的全限定名称加载文件到字节数组
byte[] classData = new byte[0];
try {
classData = getData(className);
if (classData != null) {
// defineClass方法将字节数组数据转为字节码文件
return defineClass(className, classData, 0, classData.length);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return super.findClass(className);
}
private byte[] getData(String className) throws IOException {
String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(path)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
}
- 测试自定义类加载器
java
package com.sn.knit.classloader;
import java.lang.reflect.Method;
public class TestUserClassLoader {
public static void main(String[] args) throws Exception {
UserClassLoader userClassLoader = new UserClassLoader(
"D:\File\lib");
Class c = userClassLoader.loadClass("com.sn.knit.classloader.Test");
if (c != null) {
Object o = c.newInstance();
Method m = c.getMethod("say");
m.invoke(o);
System.out.println(c.getClassLoader().toString());
}
}
}
类加载器在实际开发中的应用
- 动态模块加载:许多应用需要动态地加载类或模块,通过自定义类加载器可以实现在运行时动态加载和卸载模块,实现插件式的架构。
- 热部署:在某些场景下,需要在应用程序运行过程中更新程序代码而不需要停止整个应用程序。自定义类加载器可以用于实现热部署,动态加载新的类文件并且卸载旧的类文件,以实现更新代码而不中断服务的功能。
- Class文件加密保护:某些安全要求较高的应用场景需要保护Class文件的安全性,通过自定义类加载器可以对加载的Class文件进行解密和校验,以确保Class文件的安全加载。
- 特定类的加载扩展:有些应用需要在启动时提前加载某些类,以提高启动速度,自定义类加载器可以在启动时提前加载需要的类,以加快应用程序的启动速度。
- 扩展类加载机制:在一些特殊的应用场景下,可能需要定制化的类加载机制,通过自定义类加载器可以扩展类加载机制,实现更加灵活的加载策略。