笔记部分文字来源:
内存结构概述
简图:

- Class files:字节码文件
详细图:

注意: 方法区是JVM规范,而永久代和元空间是实现。hotspot在JDK8之前用的是永久代实现,JDK8及之后移除了永久代,用元空间实现方法区。(尚硅谷课程当中说只有hotspot有是错误的)
Quest:
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
-
类加载器
-
执行引擎
类加载器子系统
类加载子系统作用

-
类加载子系统负责从文件系统后者网络当中加载Class文件(class文件在文件开头有特定的文件标识)
-
类加载器 ClassLoader 只负责class文件的加载,是否可以运行则由 ExcutionEngine 执行引擎决定
-
加载的类的信息存放于一块称为方法区的内存空间。除了类的信息之外,方法区中还会存放运行时常量池(常量池在运行过程当中加载到内存当中称为运行时常量池)的信息,可能还包括字符串字面量和数值常量(这部分常量信息是Class文件当中常量池部分的内存映射)
类加载器ClassLoader角色

-
Class file存储在本地硬盘上(这里以一个名为Car的类为例,编译之后有对应的叫做Car.class的class文件)。
- 可以将这个理解为设计师的设计图纸,最终在执行的时候需要将这个模板加载到JVM当中,根据这个文件实例化出n个一模一样的实例。
-
Class file加载到JVM当中之后,被称为DNA元数据模板(图中就是内存当中的Car.class(椭圆形)),放在方法区。
-
在.class文件→JVM→最终成为源数据模板,此过程需要一个运输工具(类装载器ClassLoader)扮演快递员的角色。
类加载过程
概述
当前存在这样一个类:
javascript
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
它的加载过程大致如下:
-
我们的目的是执行main方法,就需要加载main方法所在类------HelloLoader
-
加载成功,则进行连接、初始化等操作。完成后调用HelloLoader类当中的main方法。
-
加载失败(编译的class字节码文件不是一个合法的字节码文件)------抛出异常

详细流程

注意:
类的加载过程本身有一个加载阶段(两者注意区分)
加载阶段
加载:
-
通过一个类全限定名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构(对照下图)
-
在内存中生成一个代表这个类的java.lang.class对象,最为方法区这个类的各种数据访问入口

加载class文件方式:
-
从本地系统当中直接加载
-
通过网络获取,典型场景:Web Applet
-
从zip压缩包当中读取,成日后jar、war格式基础
-
运行时计算生成,使用最多的是:动态代理技术
-
由其他文件生成,典型场景:JSP引用从专用数据库当中提取文件(较为少见)
-
从加密文件当中获取,典型的防class文件被反编译获取
链接阶段
链接分为三个子阶段:验证,准备,解析
验证 verify
-
目的在于确保Class文件字节流包含信息符合当前虚拟机要求,保证被加载类正确性,不会危及自身安全、
-
主要包括四种验证:
-
文件格式验证
-
元数据验证
-
字节码验证
-
符号引用验证
-
EX:
使用BinaryViewer软件查看字节码文件,开头应都是CAFE BABE ,如果出现不合法的字节码文件,那么验证将会不通过。

准备 prepare
-
为类变量(static变量)分配内存 并设置该 类变量的默认初始值 ,也就是零值
-
这里不包含final修饰的static变量,因为final修饰的在编译的时候就分配好了默认值,准备阶段会进行显示初始化。
-
注意:这里不会为实例变量分配初始化,类变变量被被方法区当中,而实例变量是随着对象一起分配到Java堆当中。
EX:
代码:变量a在准备阶段 会进行初始赋值 ,但是不是1而是0 。在初始化阶段被赋值为1。
javascript
public class HelloApp {
private static int a = 1;//prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
解析resolve
概念:
将常量池内的符号引用转换为直接引用过程。
符号引用&直接引用&相关概念
句柄
句柄(Handle)
定义:
操作系统/运行时系统提供的一个"间接引用标识",用于代表某个系统资源。
👉句柄=资源的身份证号码
👉不是资源本身而是访问资源的凭证
为什么需要句柄?(核心动机)
程序直接访问地址存在以下问题:
资源地址会变化
内存移动(GC、内存压缩)
文件重新映射
对象地址变化
直接保存地址→崩溃
- 安全问题
如果程序能够直接操作资源地址:
可以访问不该访问的内存
可以破坏系统资源
- 跨层访问困难
用户程序和操作系统之间
不能直接暴露底层地址
Java当中的句柄
1.对象句柄
JVM当中有两种对象访问方式:
方式(1):直接指针(HotSpot默认)
reference``→对象地址方式(2):句柄访问(Handle Pool)
reference``→handle→``object结构:
reference``→句柄池→对象实例+类型数据优点:
GC移动对象时,只需要修改句柄,无需修改reference
缺点:
多一次间接访问,性能略低
👉HotSpot默认不使用句柄,但是概念存在
2.Java IO当中的句柄
例如:FileInputStream当中
FileInputStream fis = new FileInputStream("a.txt");底层:Java对象→native fd(文件句柄)
3.JVM方法区/元空间当中句柄
EX:
类元数据引用
常量池引用
方法符号引用
本质都是"句柄思想"。
符号引用
定义:
一组符号描述引用的目标。这些符号可以是任何形式的字面量,只要使用是能够无歧义地定位到目标即可。
存在形式:
.class文件常量池constant pool当中。内容:包括类和接口的全限定名、字段名称和描述符、方法的名称和描述符
特点:
和JVM内存布局无关。
引用的目标不一定已经加载到内存当中
它是跨平台的,因为
.class文件只需要记录"我要找的对象叫什么"直接引用
定义:
可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
存在形式:JVM运行时的内存区域当中
内容:具体内存地址/偏移量
特点
与JVM内存布局紧密相关
有了直接引用,引用的目标一定在内存当中
同一符号引用在不同虚拟机实例上翻译出来的直接引用一般不同

上图源自ChatGPT
-
事实上,解析操作往往伴随着JVM在执行完初始化之后执行
-
解析动作主要针对的类或者接口、字段、类方法、接口方法、方法类型等。对应常量池当中的 CONSTANT Class info、CONSTANT Filedref info、CONSTANT Methodref info
初始化阶段
类的初始化时机
-
创建类的实例
-
访问某个类/接口的静态变量、对该静态变量进行赋值
-
调用类的静态方法
-
反射(EX:
Class.forName("com.atguigu.Test")) -
初始化类的子类
-
Java虚拟机启动时被标明为启动类的类
-
JDK7开始提供动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化。
除了以上七种情况,其他使用了Java类的方式都被看做是堆类的被动使用,都不会导致类的初始化------不会执行初始化阶段(不调用clint()方法、init()方法)
clinit()
clinit------ class init
初始化阶段就是执行类构造器方法clinit()方法的过程
clinit方法不需要定义,是javac编译器自动收集类当中所有类变量的赋值动作和静态代码块当中语句合并而来。------即,当我们代码当中包含static变量的时候就会存在clinit方法。
-
clinit方法当中的指令按语句在源文件当中出现的顺序执行
-
clinit不同于构造器,两者关联:构造器是虚拟机视角下的init方法
-
若该类具有父类,JVM会保证在子类的clinit方法执行之前父类的已经执行完毕。
-
虚拟机必须保证一个类的clinit方法在多线程下被同步加锁
static相关说明
含static变量

javascript
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
number = 20;
System.out.println(num);
//System.out.println(number);//报错:非法的前向引用。
//关键原因:
//java规定,在同一个类当中不能在静态变量声明之前读取改变量的值
//核心原因:
//Java判断非法引用是在编译期间完成的,编译器不关心赋值与否,而是代码的顺序规则
}
/**
* 1、linking之prepare: number = 0 --> initial: 20 --> 10
* 2、这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会
* 首先被初始化为20,再接着被初始化成10(这也是面试时常考的问题哦)
*
*/
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}
为什么这里能够操作static 变量 number?
在链接阶段的准备子阶段当中,已经为static变量进行内存的分配并设置零值;静态代码块属于初始化阶段,所以可以操作。
clinit字节码:
java
0 iconst_1
1 putstatic #3 <com/atguigu/java/ClassInitTest.num>
4 iconst_2
5 putstatic #3 <com/atguigu/java/ClassInitTest.num>
8 bipush 20 //先赋20
10 putstatic #5 <com/atguigu/java/ClassInitTest.number>
13 getstatic #2 <java/lang/System.out>
16 getstatic #3 <com/atguigu/java/ClassInitTest.num>
19 invokevirtual #4 <java/io/PrintStream.println>
22 bipush 10 //再赋10
24 putstatic #5 <com/atguigu/java/ClassInitTest.number>
27 return
当我们代码当中包含static变量的时候,就会有clinit方法。
无static变量

加上之后:

2号说明
如图所示,在init方法当执行了a、b的赋值

在构造器当中:
-
先将变量a赋值为10
-
再将局部变量b赋值为20
3号说明
若类具有父类,JVM保证在子类clinit方法执行之前父类clinit方法已经执行完毕

如上代码,加载流程如下:
-
首先,执行 main() 方法需要加载 ClinitTest1 类
-
获取 Son.B 静态变量,需要加载 Son 类
-
Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载
4号说明
虚拟机必须保障一个类的clinit方法在多线程下被同步加锁
EX:
javascript
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
输出结果:
javascript
线程2开始
线程1开始
线程2初始化当前类
/然后程序卡死了
程序卡死原因:
-
两个线程同时区加载DeadThread类,而DeadThread类当中静态代码块当中有一处while死循环
-
先加载DeadThread类的线程先抢到了同步锁,然后在类的静态代码块当中执行while死循环,而另外一个线程等待同步锁的释放
-
无论哪个线程先执行DeadThread类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)
类加载器分类
概述
JVM严格来说,支持两种类加载器:
-
引导类加载器Bootstrap ClassLoader
-
自定义类加载器User-Defined ClassLoader
从概念上面来讲,自定义类加载器一般指的是程序当中由开发人员定义的一类加载器,但是Java虚拟机规范并不是这么定义的。所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
Extend:

App/System:


最常见的三类类加载器:
-
Bootstrap
-
System
-
Extension
各个类加载器之间的层级关系如下图(这个涉及到双亲委派机制,后续讲解):
加载器之间的关系类似文件夹和内部层级、文件之间的关系

javascript
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器Bootstrap ClassLoader进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
注:
-
我们尝试通过ext获取其上层也就是引导类加载器,但是获取到的值是null,这并不代表引导类加载器不存在,而是因为引导类加载器使用的是c/c++语言,无法获取Java类对象
-
两次获取系统类加载器地址相同:
sun.misc.Launcher$AppClassLoader@18b4aac2,说明系统类加载器全局唯一
虚拟机自带的加载器
启动类加载器/引导类加载器Bootstrap ClassLoader
特点:
-
使用c/c++实现,内嵌在JVM内部
-
并不继承自java.lang.ClassLoader,没有父加载器
作用:
-
用来加载Java核心类库( JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容 ),用于提供JVM自身需要的类;EX:例如我们熟悉的java.lang\java.util\java.io
-
加载扩展类加载器、应用程序/系统类加载器,并作为他们的父类加载器
出于安全考虑,Bootstrap在双亲委派机制下优先加载核心类,同时JVM禁止用户自定义java.*等受保护的包,防止用户伪造java核心类,保障安全性。
扩展类加载器 Extension ClassLoader
特点:
-
Java语言编写,由
sun.misc.Launcher$ExtClassLoader实现 -
派生于ClassLoader类
-
父类加载器为BootStrap ClassLoader
作用:
-
从java.ext.dirs系统属性所指定的目录当中加载类库,或从JDK安装目录的jre/lib/ext子目录(扩展目录)下加载类库。(简而言之,就是加载Java平台的扩展类库,用户Bootstrap和Application/System之间的缓冲)
-
若用户创建的JAR放在ext目录下面,自动由扩展类加载器加载
系统/应用程序类加载器 System/App ClassLoader
特点:
-
Java编写,由sun.misc.LaunchersAppClassLoader实现
-
派生于ClassLoader类
-
父类加载器为Extension ClassLoader
作用:
-
应用程序的类路径(classpath)中的类和资源
-
编写的.class文件(类文件)
-
依赖的jar包
-
classpath下的资源文件(properties、yaml、xml等)
-
-
该类加载器是程序当中的默认类加载器,一般来说,Java应用的类都是由它加载完成加载
通过classLoader.getSystemClassLoader()方法能够获取该类加载器
代码举例
javascript
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
输出结果
javascript
**********启动类加载器**************
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/classes
null
***********扩展类加载器*************
C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@29453f44
用户自定义类加载器
为什么需要用户自定义类加载器?
Java开发的日常程序当中,类的加载几乎都是上述三种类加载器配合完成的。我们通过自定义类加载器顶之类的加载方式:
-
隔离加载类(假设,我们项目当中使用Spring框架+RocketMQ消息队列中间件,两者存在包名、类名、路径完全一样的类,这时候两个类就出现冲突。不过主流的框架和中间件都会自定义类加载器,实现不同的框架,中间件之间是隔离的)
-
修改类的加载方式
-
扩展加载源(可以考虑从数据库、路由器等不同地方加载类)
-
防止源码泄露(对字节码文件进行加密,之后通过自定义类加载器进行解密之后使用)
如何自定义类加载器
开发人员通过编写继承抽象类java.lang.ClassLoader的类的方式实现,满足特殊需求
-
JDK1.2之前,在自定义类加载器当中,总会去继承ClassLoader并重写loadClass()方法,从而实现自定义的类的加载。但是在JDK1.2之后不再建议用户去覆盖loadClass方法,而是建议吧自定义的类的加载逻辑写在findClass方法当中
-
编写自定义类加载器的时候,,如果没有太过于复杂的要求,可以直接继承URIClassLoader类,避免自己编写findClass方法以及其获取字节码流的方式,让自定义类加载器更加简洁。
EX:
javascript
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
//通过defineClass方法将字节码转换为Java的Class对象,成为一个真正的类
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) {
//创建自定义类加载器
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
//强制通过自定义类加载器加载类One
//参数1:类全限定名
//参数2:是否初始化类
//参数3:指定类的加载器
Class<?> clazz = Class.forName("One", true, customClassLoader);
//创建对象
Object obj = clazz.newInstance();
打印类加载器
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
关于ClassLoader
介绍
ClassLoader,抽象类,除BootStrap ClassLoader启动类加载器以外所有类加载器都继承自ClassLoader

和相关类加载器关系(箭头依旧指的是双亲委派机制当中的关系)
javascript
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
获取ClassLoader的途径

javascript
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
javascript
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
Process finished with exit code 0
注意:
javascript
System.out.println(Thread.class.getClassLoader());
输出是null,这是因为Thread属于Java.lang包,是JVM核心类,由BootStrap加载
但是上面举例代码当中,输出的是线程上下文加载器,而JVM将AppClassLoader作为main线程的ContextClassLoader
双亲委派机制
原理
Java虚拟机对class文件采用按需加载 方式,也就是说,当需要使用该类时才会将class文件加载到内存生成class对象。而加载某个类的class文件时,JVM使用双亲委派机制 ,将请求交给父类处理。这是一种任务委派模式:
-
如果一个类加载器收到任务加载需求,并不会直接自己先去加载,而是把这个请求先交给父类加载器处理
-
父类加载器存在其父类加载器,进一步向上委托,依次递归,请求最终到达顶层启动类加载器。
-
父类加载器能成功完成类加载任务,成功返回。若父类加载器无法完成任务,子类(当前)加载器自己尝试加载。
-
父类无法完成加载任务,返回给当前加载器尝试加载,层层(父类、父类的父类。。。)尝试都失败,抛出异常ClassNotFountException。
EX:
javascript
AppClassLoader.loadClass()
↓
委派给 ExtClassLoader
↓
委派给 BootstrapClassLoader
↓
BootstrapClassLoader 尝试加载
↓
如果失败(ClassNotFoundException)
↑
返回 ExtClassLoader
↓
ExtClassLoader 尝试加载
↓
如果失败
↑
返回 AppClassLoader
↓
AppClassLoader.findClass() 自己加载

双亲委派机制代码演示
EX1
我们先自己创建一个java.lang.String类,写上static代码块(java.lang就是软件包名称)
javascript
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
在另外的程序当中加载String类,检查String是否是JDK当中自带的
javascript
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
}
}
输出结果:
javascript
hello,atguigu.com
没有输出静态代码块内容,说明加载的是JDK的而不是我们自定义的
之后我们修改自定义的String,添加main方法,而JDK自身不自带main方法
javascript
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
点击执行main方法,出现以下输出:

原因:
由于双亲委派机制,最终会由Bootstrap加载JDK自带的String,之后尝试执行main方法,1发现JDK自带的String并没有main方法,出现异常
EX2
这次我们尝试继续创建java.lang包下的类但是不重名
javascript
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
输出:
javascript
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
Process finished with exit code 1
原因:
即使类名不重复,Java也禁止使用java.lang包名,从而保护核心类安全。
特例流程
当我们加载jdbc.jar包用于实现数据库连接的时候:
-
当前程序需要SPI接口(因为jvm需要连接各种数据库,所以提供SPI接口,而由各个第三方实现接口),SPI接口本身属于rt.jar包中Java核心api
-
我们使用双亲委派机制,引导类加载器加载rt.jar包当中SPI接口(JVM启动时就已经进行加载)
-
具体实现类涉及到第三方jar包,EX:我们加载SPI实现类jdbc.jar
-
第三方jar包中类通过系统类加载器加载
- 由上得知:SPI核心接口由引导类加载器加载,而具体实现类由系统加载器加载

为什么是"反向委托"
由于双亲委派机制是不会"回头的",而是父类加载全部失败则返回当前类加载器加载
而在jdbc当中:
jdbc类库本身在java.sql.Driver当中,属于是JDK的核心库,由引导类加载器加载
但是由于我们需要数据库驱动连接数据库,而数据库驱动:
在mysql-connector.jar包当中,通过系统类加载器加载,而引导类无法加载、线程上下文加载器可以加载,所以需要绕过双亲委派机制加载
关键机制:SPI+ContextClassLoader反向委派
JDK机制:
Thread.currentThread().getContextClassLoader()获取当前线程上下文类加载器(默认是AppClassLoader),让BootstrapClassLoader借用AppClassLoader加载驱动类
双亲委派机制优势
-
避免类被重复加载(多个类加载器加载)
-
保护程序安全,防止核心API被篡改
沙箱安全机制
- 自定义String类时,在加载自定义String类时率先使用引导类加载器加载,而引导类加载器加载的时率先加载JDK自带的文件------rt.jar当中java.lang.String.class文件,因此出现报错:String当中没有main方法
通过沙箱安全机制,保障对java核心安全代码保护
Questions
如何判断两个类是否相同
JVM当中表示两个class对象是否同为一个类存在的必要条件:
-
类完整名必须一致(包括包名)
-
加载这个类的类加载器完全相同(类加载器实例对象)
换句话说,在 JVM 中,即使这两个类对象(class对象)来源同一个Class文件,被同一个 虚拟机 所加载,但只要加载它们的ClassLoader实例对象不同(例如TOMCAT部署多个Web应用,每个应用都有自己的classpath,可能存在同名类),那么这两个类对象也是不相等的
对类加载器的引用
JVM必须知道一个类是由启动类加载器加载还是用户自定义类加载器加载的
-
若一个类由用户自定义类加载器加载, JVM 会将这个类加载器的一个引用作为类的信息一部分保存在方法去中
-
当解析一个类引用了另外一个类的时候,JVM需要保证:
-
要么两个类的类加载器相同(同类型但是不是同一个也不行❌)
-
要么被引用的类的加载器是当前类加载器的父加载器(子→父,✔;父→子,❌)
-
上图源自ChatGPT