JVM 的结构基本上由 5 部分组成:
class文件
解析后生成一个Class对象
加载到到 JVM 中字节码指令
,相当于实际机器上的 CPU
模拟实际机器上的存储
、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等,由Heap、程序计数器、虚拟机栈、本地方法栈和方法区
五部分组成。 C 或 C++ 实现的本地方法
的代码返回结果无用对象的释放
,主要是堆内存和方法区
JVM被分为三个主要的子系统:类加载器子系统、运行时数据区、执行引擎
Java代码编译执行过程
Javac命令
)将Java代码编译成Jvm字节码(.class文件)过ClassLoader
及其子类来完成Jvm的类加载
Java命令
)解释执行什么是类的加载
.class
文件中的二进制数据读入到内存
中,并对class文件中的数据进行校验、转换、解析、初始化
等操作后,将其放在运行时数据区
的 方法区内
,然后在堆
区创建一个 Java.lang.Class
对象,封装类在方法区内的数据结构。类从JVM加载到卸载出内,整个生命周期包括:加载、验证、准备、解析、初始化
、使用和卸载 7个阶段
全路径
查找此类字节码文件,并利用class文件
创建对应的Class对象
并加载到方法区
虚拟机规范
,而不会危害虚拟机自身运行安全。主要包括4种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
。final在编译的时候就会分配了
,且这里不会为实例变量分配初始化
,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆
中。常量池
中的符号引用
替换为直接引用
的过程。
静态链接过程
(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。例如:在com.xxx.Person类中引用了com.xxx.Animal类,在编译阶段Person类
并不知道Animal的实际内存地址
,因此只能用com.xxx.Animal来代表Animal真实的内存地址
。而在解析阶段,JVM可以通过解析该符号引用
,来确定com.xxx.Animal类的真实内存地址(如果该类未被加载过,则先加载)。
符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是
直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
。
初始化阶段是类加载过程的最后一步
,这一步才 真正开始执行
类中定义的Java程序代码。
即对类的静态变量初始化为指定的值,执行静态代码块
。在之前的准备阶段
,类中定义的static静态变量已经被赋过一次默认值
。而在初始化阶段,则会调用类构造器
来完成初始化操作,为静态变量赋原始值。
类构造器
和类构造方法
区别。
类构造方法
每创建一次对象,就自动调用一次,而类构造器
类似于一个无参的构造函数
,只不过该函数是静态修饰
,只会初始化静态所修饰的代码
。public class Student{
public static int x = 10;
public String zz = "jiaboyan";
static{
System.out.println("12345");
}
}
类构造器
是由编译器自动收集类中的所有静态变量的赋值动作
和静态语句块static{}
中的代码合并
产生的,编译器收集的顺序
是由 语句在源文件中出现的顺序
所决定的综上所述,对于上面的例子来说,类构造器
为:
public class Student{
<clinit>{
public static int x = 10;
System.out.println("12345");
}
}
JVM规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有
下列5种情况必须对类进行初始化(加载、验证、准备都会随着发生)
:
遇到 new、getstatic、putstatic、invokestatic
这4条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。
最常见的生成这 4 条指令的场景是:
1.
使用 new 关键字实例化对象的时候;2.
读取或设置类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候3.
调用一个类的静态方法的时候。
使用 java.lang.reflect 包
的方法对类进行反射调用
的时候,如果类没有进行初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化
,则需要先触发其父类的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类(启动类)
(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
当使用 JDK.7 的动态语言
支持时,如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
以上 5 种场景中的行为称为对一个类进行主动引用
。除此之外,所有引用类的方式都不会触发初始化,称为被动引用
。被动引用的常见例子包括:
通过子类引用父类的静态字段
,不会导致子类初始化。通过数组定义来引用类
,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。常量在编译阶段会存入调用类的常量池中
,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。特殊情况
它所有的父类都被初始化
,但这一条规则并不适用于接口。一个父接口并不会因为他的子接口或者实现类的初始化而初始化,只有当程序首次被使用特定接口的静态变量时,才会导致该接口的初始化。代码块、静态方法、构造方法加载顺序
public class Son extends Father{
static { System.out.println("子类静态代码块"); }
{ System.out.println("子类代码块"); }
public Son() { System.out.println("子类构造方法"); }
public static void main(String[] args) {
new Son();
}
}
class Father{
static { System.out.println("父类静态代码块"); }
{System.out.println("父类代码块");}
public Father() { System.out.println("父类构造方法");}
public static void find() {
System.out.println("静态方法");
}
}
//代码块和构造方法执行顺序
//父类静态代码块
//子类静态代码块
//父类代码块
//父类构造方法
//子类代码块
//子类构造方法
C++
实现的,是虚拟机自身的一部分,负责加载 \lib\ 目录下的核心类库
或被 -Dbootclaspath 参数指定的类
, 如: rt.jar, tool.jar 等sun.misc.Launcher$ExtClassLoader类
中实现, 负责加载 \lib\ext 扩展目录中的类库
或 -Djava.ext.dirs 选项所指定目录下的类和 jar包
,开发者可以直接使用标准扩展类加载器,//Launcher$ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
//加载/lib/ext目录中的类库
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st = new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
sun.misc.Launcher$AppClassLoader
中实现, 负责加载 环境变量 CLASSPATH
或 -Djava.class.path、java -cp所指定的目录下的类和 jar 包
。可以通过ClassLoader#getSystemClassLoader()
获取到该类加载器,开发者可以直接使用系统类加载器,该加载器是程序中默认类加载器
,public class MainClassLoaderTest {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader());
System.out.println(MainClassLoaderTest.class.getClassLoader());
/*null
sun.misc.Launcher$ExtClassLoader@246b179d
sun.misc.Launcher$AppClassLoader@18b4aac2*/
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();//应用程序类加载器
ClassLoader extClassLoader = appClassLoader.getParent();//拓展类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();//引导类加载器
System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);
System.out.println("extClassLoader: " + extClassLoader);
System.out.println("appClassLoader: " + appClassLoader);
/*bootstrapClassLoader: null
extClassLoader: sun.misc.Launcher$ExtClassLoader@246b179d
appClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2*/
//BootstrapLoader 父加载器 及 加载路径
System.out.println();
System.out.println("bootstrapLoader 加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
/*bootstrapLoader 加载以下文件:
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/resources.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/rt.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/sunrsasign.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jsse.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jce.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/charsets.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jfr.jar
file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/classes*/
//ExtClassLoader 父加载器 及 加载路径
System.out.println();
System.out.println("extClassLoader 加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
/*extClassLoader 加载以下文件:
E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\ext;C:\Windows\Sun\Java\lib\ext*/
//AppClassLoader父加载器 及 加载路径
System.out.println();
System.out.println("appClassLoader 加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
/*appClassLoader 加载以下文件:
E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\charsets.jar;
省略.....
E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\rt.jar;
省略
E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\resources.jar;
省略
E:\04_resource_study\java_base_demo\target\classes;
省略
E:\dev_tools\mavenLocalStorage\org\springframework\boot\spring-boot-starter-web\2.2.0.RELEASE\spring-boot-starter-web-2.2.0.RELEASE.jar;
E:\dev_tools\mavenLocalStorage\com\alibaba\fastjson\1.2.78\fastjson-1.2.78.jar;
E:\dev_tools\mavenLocalStorage\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar;*/
}
}
单例设计模式
,其中BootstrapClassLoader 、ExtClassLoader和AppClassLoader的扫描路径分别对应系统属性sun.boot.class.path、java.ext.dirs、java.class.path
加载用户自定义包路径下的类包
,通过 ClassLoader 的子类实现 Class 的加载。继承抽象类ClassLoader
,重写findClass方法
,判断当前类的class文件是否已被加载/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
//包路径
private String path;
//构造方法,用于初始化Path属性
public MyClassLoader(String path) {
this.path = path;
}
//重写findClass方法,参数name表示要加载类的全类名(包名.类名)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//检查该类的class文件是否已被加载,如果已加载则返回class文件(字节码文件)对象,如果没有加载返回null
Class<?> loadedClass = findLoadedClass(name);
//如果已加载直接返回该类的class文件(字节码文件)对象
if (loadedClass != null) {
return loadedClass;
}
//字节数组,用于存储class文件的字节流
byte[] bytes = null;
try {
//获取class文件的字节流
bytes = getBytes(name);
} catch (Exception e) {
e.printStackTrace();
}
if (bytes == null) {
throw new ClassNotFoundException();
}
//如果字节数组不为空,则将class文件加载到JVM中
//将class文件加载到JVM中,返回class文件对象
//字节码数组加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法
return this.defineClass(name, bytes, 0, bytes.length);
}
//获取class文件的字节流
private byte[] getBytes(String name) throws Exception {
//拼接class文件路径 replace(".",File.separator) 表示将全类名中的"."替换为当前系统的分隔符,File.separator返回当前系统的分隔符
String fileUrl = path + name.replace(".", File.separator) + ".class";
//缓冲区
byte[] buffer = new byte[1024];
//输入流
InputStream fis = new FileInputStream(new File(fileUrl));
//相当于一个缓存区,动态扩容,也就是随着写入字节的增加自动扩容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//循环将输入流中的所有数据写入到缓存区中
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
baos.flush();
baos.close();
return baos.toByteArray();
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
String path = "E:/04_resource_study/java_base_demo/target/classes/";
//创建自定义类加载器对象
MyClassLoader classLoader = new MyClassLoader(path);
System.out.println("MyDamageClassLoader的父加载器:" + classLoader.getParent());
//返回加载的class对象
Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");
//调用类的构造方法创建对象
Object o = clazz.newInstance();
//输出创建的对象
System.out.println("创建的对象:"+o);
//输出当前类加载器
System.out.println("ClassLoaderTest当前类加载器:"+clazz.getClassLoader());
//输出当前类加载器的父类
System.out.println("ClassLoaderTest当前类加载器的父类:"+clazz.getClassLoader().getParent());
//输出当前类加载器的父类的父类
System.out.println("ClassLoaderTest当前类加载器的父类的父类:"+clazz.getClassLoader().getParent().getParent());
//输出当前类加载器的父类的父类的父类
System.out.println("ClassLoaderTest当前类加载器的父类的父类的父类:"+clazz.getClassLoader().getParent().getParent().getParent());
/*
执行结果:
MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
创建的对象:com.demo.classload.ClassLoaderTest@238e0d81
ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader@7daf6ecc
ClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader@31221be2
ClassLoaderTest当前类加载器的父类的父类的父类:null
*/
}
}
为什么要自定义类加载器?
一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能
)而是每个类加载器通过组合方式维护一个 parent字段,指向父加载器
。如果parent=null,则它的父级就是启动类加载器。
)AppClassLoader。
按需加载: JVM对Class文件采用的是按需加载
的方式,也就是说当需要使用该类时才会将它的Class文件加载到内存生成Class对象
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责一起加载
,除非显示使用另外一个类加载器来载加载
父类委托(双亲委派):先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类(为了避免不同的加载器 加载相同的类 出现重复字节)
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
在JVM中表示两个class对象是否为同一个类对象存在2个必要条件
- 类的全限定名必须一致(即包名.类名)
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
即JVM中两个类对象(class对象)就算来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间
,所以加载的class对象也会存在不同的类名空间中
,但前提是重写loadClass()
, 因为双亲委派模型第一步会通过Class c = findLoadedClass(name)从缓存查找,类全限定名相同则不会再次被加载
,因此我们必须跳过缓存查询
才能重新加载class对象。当然也可直接调用findClass()
方法,这样也避免从缓存查找.
JVM 的类加载器具有父子关系,双亲委派机制是在Java 1.2
后引入的,其工作原理的是 ,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器(Bootstrap Class Loader),如果启动类加载器可以完成类加载任务,就成功返回,否则就一层一层向下委派子加载器去尝试加载,这就是双亲委派模式
,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成
,这不就是传说中的实力坑爹啊?
那么采用这种模式有啥用呢?
自下而上的委派,又自上而下的加载
,保证每一个类在各个类加载器中都是同一个类
。JVM 并不是在启动时就把所有的.class文件都加载一遍,而是程序在运行过程中用到了这个类才去加载
。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader
,这个抽象类中定义了3个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader {
//委托的父类加载器
private final ClassLoader parent;
//name为类的全限定名(包名.类名)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//name为类的全限定名(包名.类名) ,resolve如果为true,则在生成class对象的同时进行解析相关操作
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//同步控制:开启并发加载的情况下,锁对类对应的锁对象,否则为ClassLoader对象本身
synchronized (getClassLoadingLock(name)) {
//首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
//1.如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
//如果有父加载器,则先委派给父加载器去加载(注意这里是递归调用)
if (parent != null) {
c = parent.loadClass(name, false);
// 如果父加载器为空,则委托启动类加载器去加载
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果没有从非空父加载器中找到类,则抛出类异常ClassNotFoundException
}
//2.如果父加载器没加载成功,调用自己实现的findClass去加载
if (c == null) {
//如果仍然没有找到,则调用findClass来查找类。
long t1 = System.nanoTime();
c = findClass(name);
//这是定义类装入器;记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否生成class对象的同时进行解析相关操作
if (resolve) {
resolveClass(c);
}
return c;
}
}
//name为类的全限定名(包名.类名)
protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
//......省略具体实现......
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
//将字节码数组解析成一个Class对象,用native方法实现
//b:class文件字节数组,off:开始读取位置,len:每次字节数
protected final Class<?> defineClass(byte[] b, int off, int len){
//......省略具体实现......
}
//获取类名对应的锁对象
//ClassLoader并发加载是通过一个ConcurrentHashMap实现的,Key为类名,对应的Value为一个new Object(),
//所以它可以同时加载多个类,但同一个类重复加载时则可以锁住。通过registerAsParallelCapable()可以启用并发加载。
//详见https://blog.csdn.net/w1673492580/article/details/81912344
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
}
从上面的代码可以得到几个关键信息:
而是每个类加载器通过组合方式维护一个 parent字段,指向父加载器
。
调用 native 方法
把 Java 类的字节码解析成一个 Class 对象。找到.class文件并把.class文件读到内存得到字节码数组
,然后在方法内部调用 defineClass方法得到 Class 对象
子类必须实现findClass
。实现双亲委派机制
:首先检查类是不是被加载过,如果加载过直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载
。抽象类ClassLoader类
,除启动类加载器外,所有的类加载器都继承自ClassLoader
,这里主要介绍ClassLoader中几个比较重要的方法。该方法就是双亲委派机制的具体实现,用于加载指定全限定名的类,由ClassLoader类实现,在JDK1.2之后不建议开发者重写但可以直接通过this.getClass().getClassLoder.loadClass("className")
调用,源码如下:
先从缓存中查找该类对象,如果存在直接返回
,如果不存在则交给该类加载器去的父加载器去加载
,倘若没有父加载器则交给顶级启动类加载器
去加载,最后倘若仍没有找到,则使用findClass()
方法去加载 //name为类的全限定名(包名.类名)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//name为类的全限定名(包名.类名) ,resolve如果为true,则在生成class对象的同时进行解析相关操作
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//同步锁
synchronized (getClassLoadingLock(name)) {
//首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
//1.如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
//如果有父加载器,则先委派给父加载器去加载(注意这里是递归调用)
if (parent != null) {
c = parent.loadClass(name, false);
// 如果父加载器为空,则委托启动类加载器去加载
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果没有从非空父加载器中找到类,则抛出类异常ClassNotFoundException
}
//2.如果父加载器没加载成功,调用自己实现的findClass去加载
if (c == null) {
//如果仍然没有找到,则调用findClass来查找类。
long t1 = System.nanoTime();
c = findClass(name);
//这是定义类装入器;记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否生成class对象的同时进行解析相关操作
if (resolve) {
resolveClass(c);
}
return c;
}
}
该方法在JDK1.2之后已不建议用户去覆盖loadClass()方法
,而是建议把自定义的类加载逻辑写在findClass()方法中
loadClass()
中被调用的,在loadClass()
中当父加载器
加载失败后,则会调用自己的findClass()
方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。findClass()
方法,只是简单的抛出ClassNotFoundException
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);//直接抛出异常
}
把class文件读到内存得到字节码数组
后,需要在该方法内部调用 defineClass()方法得到 Class 对象该方法由ClassLoader类中实现,用于将byte字节流解析成JVM能够识别的Class对象
(),通常与findClass()方法一起使用
解析
(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。代码示例
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
Class对象创建完成也同时被解析
。前面我们说链接阶段主要是对字节码进行验证
,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用
。URLClassLoader是ClassLoade的具体实现
,并新增URLClassPath类
辅助获取Class字节码流
等功能,如果自定义类加载器时,没有太复杂的需求,可以直接继承URLClassLoader类
,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
避免类的重复加载:当父类加载器已经加载类后子类加载器没必要再次加载,保证了类加载的唯一性。
保证 Java 核心库的类型安全:Java 核心库中的类加载工作都是由启动类加载器
统一来完成的。从而确保了Java 应用所使用的都是同一个版本的 Java 核心类库,他们之间是相互兼容的。
java.lang.Object
,存在于rt.jar
中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载
,因此Object类在程序的各种类加载器环境中都是同一个类。不同的类加载器可以为相同类(binary name)的类
创建额外的命名空间。相同名称的类可以并存在JVM中,只需要不同的类加载器来加载他们即可,不同的类加载器的类之间是不兼容的,这相当于在JVM内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际运用。
拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都继承自URLClassLoader
,是sun.misc.Launcher的静态内部类
。
启动主应用程序
ExtClassLoader并没有重写loadClass()方法
,这说明其遵循双亲委派模式
,而AppClassLoader重载了loadCass()方法
,但最终调用的还是父类loadClass()方法
,因此依然遵守双亲委派模式,重载方法源码如下:
/**
* Override loadClass 方法,新增包权限检测功能
*/
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
//依然调用父类的方法
return (super.loadClass(name, resolve));
}
在Java类加载器基础上,Tomcat新增了几个类加载器,包括3个基础类加载器和每个Web应用的私有类加载器,其中3个基础类加载器可在conf/catalina.properties
中配置,具体介绍下:
Common Class Loader::以System Class Loader
为父类,是tomcat顶层的公用类加载器,其路径由conf/catalina.properties
中的common.loader指定,默认加载$CATALINE_HOME/lib
下的包。
Catalina Class Loader:以Common Class Loader
为父类,是用于加载Tomcat应用服务器的类加载器,其路径由server.loader
指定,默认为空,此时tomcat使用Common Class Loader
加载应用服务器。
Shared ClassLoader:以Common Class Loader
为父类,是所有Web应用的父加载器,其路径由shared.loader
指定,默认为空,此时Tomcat使用Common Class Loader
类加载器作为Web应用的父加载器。
Webapp ClassLoader: 以Shared ClassLoader
为父类,加载/WEB-INF/classes
目录下的未压缩的Class
和 资源文件
以及 /WEB-INF/lib目录下的jar包
,是各个 Webapp 私有的类加载器
, 加载路径中的 class 只对当前 webapp 可见
WEB-INF/libs
目录下的jar包, 因此不同的web应用包不会冲突。如A应用用的是spring 4.X , B应用用的是spring 5.X , 他们可以在同一个tomcat中运行默认情况下,Common、Catalina、Shared类加载器是同一个
,但可以配置3个不同的类加载器,使他们各司其职。
一个Tomcat 是可以部署多个 war 包的
,如果部署的多个war包中依赖的Jar包是不同版本的,比如War包A依赖 Spring 4,War包B依赖 Spring5 ,这时根据双亲委派机制,Spring4首先被加载进来,那么另一个依赖 Spring 5 的 War包B在加载时就不会再去加载 Spring 5 。因为同名的原因会直接给他返回已加载过的 Spring 4 。这时会出现版本不一致
的问题。因此对于 Tomcat 来说他就需要 自己实现类加载器来打破双亲委派模型
,并给每一个war包去生成一个自己对应的类加载器如何打破?
Thread.currentThread().setContextClassLoader(xx)
设置成了Catalina ClassLoader
,使用Catalina ClassLoader来加载Tomcat使用的类,当Tomcat加载WebApp中的类时设置成了WebappClassLoader
,而WebappClassLoader重写了loadClass方法打破了双亲委派
。Web应用默认的类加载顺序是(打破了双亲委派规则):
/WEB-INF/classes
中的类。/WEB-INF/lib/
中的jar包中的类。如果在配置文件中配置了
,那么就是遵循双亲委派规则,加载顺序如下:
/WEB-INF/classes
中的类。/WEB-INF/lib/
中的jar包中的类。线程上下文类加载器(ContextClassLoader)是JDK 1.2
开始引入一种类加载器传递机制。可以通过Thread.currentThread().setContextClassLoader(xx)
方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能通过Thread.currentThread().getContextClassLoader()
获取该类加载器使用。
为了方便破坏双亲委派
,如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器
,默认
线程的上下文类加载器是系统类加载器(AppClassLoader)
, 在线程中运行的代码可以通过该
类加载器来加载类和资源双亲委派模型是Java设计者推荐给开发者们的默认类加载器实现方式
。这个委派和加载顺序是非强制性,可破坏的。主要有2种方式
自定义类加载器,继承ClassLoader
,重写findClass()
,然后再重写loadClass()
改变双亲委派的类加载顺序。
通过线程上下文类加载器
的传递性,让父类加载器中调用子类加载器的加载动作。
下面使用方式1,基于代码 4.自定义类加载器(Custom Class Loader)重写loadClass()
即可
public class MyClassLoader extends ClassLoader {
//......................省略其他代码......................
//重点: 重写loadClass()改变双亲委派的类加载顺序
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
//1.破坏双亲委派的位置:自定义类加载机制先委派给ExtClassLoader加载,ExtClassLoader再委派给BootstrapClassLoader,如果都加载不了,然后自定义类加载器加载,自定义类加载器加载不了才交给SystemClassLoader
ClassLoader classLoader = getSystemClassLoader();
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
}
Class<?> clazz = null;
try {
clazz = classLoader.loadClass(name);
} catch (ClassNotFoundException e) {
// Ignore
}
if (clazz != null) {
return clazz;
}
//2.自己加载
clazz = this.findClass(name);
if (clazz != null) {
return clazz;
}
// 3.自己加载不了,再调用父类loadClass,保持双亲委派模式
return super.loadClass(name);
}
//......................省略其他代码......................
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
String path = "E:/04_resource_study/java_base_demo/target/classes/";
//创建自定义类加载器对象
MyClassLoader classLoader = new MyClassLoader(path);
System.out.println("MyDamageClassLoader的父加载器:" + classLoader.getParent());
//返回加载的class对象
Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");
//调用类的构造方法创建对象
Object o = clazz.newInstance();
//输出创建的对象
System.out.println("创建的对象:"+o);
//输出当前类加载器
System.out.println("ClassLoaderTest当前类加载器:"+clazz.getClassLoader());
//输出当前类加载器的父类
System.out.println("ClassLoaderTest当前类加载器的父类:"+clazz.getClassLoader().getParent());
//输出当前类加载器的父类的父类
System.out.println("ClassLoaderTest当前类加载器的父类的父类:"+clazz.getClassLoader().getParent().getParent());
//输出当前类加载器的父类的父类的父类
System.out.println("ClassLoaderTest当前类加载器的父类的父类的父类:"+clazz.getClassLoader().getParent().getParent().getParent());
/*
执行结果:
MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
创建的对象:com.demo.classload.ClassLoaderTest@2e5d6d97
ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader@685f4c2e
ClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader@238e0d81
ClassLoaderTest当前类加载器的父类的父类的父类:null
null
*/
}
}
重写loadClass()后,
类加载时先委派给ExtClassLoader加载,ExtClassLoader再委派给BootstrapClassLoader,如果都加载不了,然后自定义类加载器加载,自定义类加载器加载不了才交给SystemClassLoader。
为什么不能直接让自定义类加载器加载呢?
不能!双亲委派的破坏只能发生在SystemClassLoader及其以下的加载委派顺序,ExtClassLoader上面的双亲委派是不能破坏的!
java.lang.Object
,而加载一个类时,也会加载继承的类
,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器
。如ClassLoaderTest类只隐式继承了Object
,自定义MyDamageClassLoader 加载了ClassLoaderTest,也会加载Object。
如果loadClass()
直接调用MyDamageClassLoader的findClass()
会报错java.lang.SecurityException: Prohibited package name: java.lang
目前比较常见的场景主要有:
线程上下文类加载器(Thread.currentThread().setContextClassLoader(xx)、Thread.currentThread().getContextClassLoader()
),如:JDBC 使用线程上下文类加载器加载 Driver 实现类
Tomcat 的多 Web 应用程序
OSGI 实现模块化热部署
热部署类加载器: 即利用同一个class文件不同的类加载器在内存创建出两个不同的class对象
JVM在加载类之前会检测请求类是否已加载过(即在loadClass()
方法中调用findLoadedClass()
方法),如果被加载过,则直接从 缓存
获取,不会重新加载。
前面的自定义类加载器MyClassLoader通过直接调用findClass()
方法已具备这个热加载功能
findClass()
来实现热加载,而不是loadClass()
呢?
ClassLoader
中默认实现的loadClass()
方法中调用findLoadedClass()
方法进行了检测是否已被加载
,因此直接调用findClass()
方法就可以绕过重用class缓存问题, 当然也可以重写loadClass()方法
,但强烈不建议这么干注意; 同一个class文件最多只能被同一个类加载器的实例调用findClass()
加载一次,多次加载将报错
, 因此必须让同一类加载器的不同实例加载同一个class文件
,以实现所谓的热部署。
public class FileClassLoader extends ClassLoader {
/**
* 根路径
*/
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 重写findClass逻辑
*
* @param className
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
//获取类的class字节数组,用于存储class文件的字节流
byte[] bytes = null;
try {
//获取class文件的字节流
bytes = getBytes(className);
} catch (Exception e) {
e.printStackTrace();
}
//如果文件为空
if (bytes == null) {
throw new ClassNotFoundException();
}
//直接生成class对象
return defineClass(className, bytes, 0, bytes.length);
}
//获取class文件的字节流
private byte[] getBytes(String className) throws IOException {
// 读取类文件的字节
String path = classNameToPath(className);
//缓冲区
byte[] buffer = new byte[1024];
//输入流
InputStream fis = new FileInputStream(new File(path));
//相当于一个缓存区,动态扩容,也就是随着写入字节的增加自动扩容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//循环将输入流中的所有数据写入到缓存区中
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
baos.flush();
baos.close();
return baos.toByteArray();
}
/**
* 类文件的完全路径
* @param className
* @return
*/
private String classNameToPath(String className) {
//拼接class文件路径 replace(".",File.separator) 表示将全类名中的"."替换为当前系统的分隔符,File.separator返回当前系统的分隔符
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
测试方法
public static void main(String[] args) {
//类所在包的绝对路径
String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";
//类的全限定名
String classPath = "com.demo.classload.ClassLoaderTest";
//创建自定义文件类加载器
FileClassLoader loaderA = new FileClassLoader(rootDir);
FileClassLoader loaderB = new FileClassLoader(rootDir);
try {
//加载指定的class文件,调用loadClass()
Class<?> objectL1 = loaderA.loadClass(classPath);
Class<?> objectL2 = loaderB.loadClass(classPath);
System.out.println("loadClass->object1:" + objectL1.hashCode());
System.out.println("loadClass->object2:" + objectL2.hashCode());
Class<?> object3 = loaderA.loadClass(classPath);
Class<?> object4 = loaderB.loadClass(classPath);
System.out.println("loadClass->obj3:" + object3.hashCode());
System.out.println("loadClass->obj4:" + object4.hashCode());
Class<?> object5 = loaderA.findClass(classPath);
Class<?> object6 = loaderB.findClass(classPath);
System.out.println("findClass->object5:" + object5.hashCode());
System.out.println("findClass->object6:" + object6.hashCode());
//执行到这里会报错: java.lang.LinkageError: loader (instance of com/demo/classload/FileClassLoader): attempted duplicate class definition for name: "com/demo/classload/ClassLoaderTest"
//原因: 同一个class文件最多只能被同一个类加载器的实例调用`findClass()`加载一次,多次加载将`报错`, 因此必须让`同一类加载器的不同实例加载同一个class文件`,以实现所谓的热部署。
// Class> object7 = loaderA.findClass(classPath);
// Class> object8 = loaderB.findClass(classPath);
FileClassLoader loaderC= new FileClassLoader(rootDir);
FileClassLoader loaderD = new FileClassLoader(rootDir);
Class<?> object7 = loaderC.findClass(classPath);
Class<?> object8 = loaderD.findClass(classPath);
System.out.println("findClass->object7:" + object7.hashCode());
System.out.println("findClass->object8:" + object8.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
// 执行结果
// loadClass->object1:1751075886
// loadClass->object2:1751075886
// loadClass->obj3:1751075886
// loadClass->obj4:1751075886
// findClass->object5:930990596
// findClass->object6:1921595561
// findClass->object7:87285178
// findClass->object8:610998173
}
结论:
loadClass()
加载class,每次返回的都是同一个class实例findClass()
加载class,第2次调用会抛出异常 java.lang.LinkageError
, 因此每次热部署都要new一个新类加载器实例来调用findClass()
加载class,以返回不同的class实例热部署加载监控线程
type()
方法中,println打印内容,然后将重新编译后的Pay.class
文件放到指定类加载路径中就行了package com.demo.classload;
public class Pay {
public void type() {
System.out.println("微信支付");
}
}
@Slf4j
public class HotDeploymentTest {
public static void main(String[] args) throws InterruptedException {
//记录文件上次修改时间
AtomicReference<Long> lastModified = new AtomicReference<>(0L);
new Thread(() -> {
while (true) {
//类文件所在包根路径
String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";
//类文件所在包绝对路径
String classAbsolutePath = rootDir + "/com/demo/classload/Pay.class";
//类的全限定名
String classSourcePath = "com.demo.classload.Pay";
try {
File file = new File(classAbsolutePath);
//文件不存在,线程休眠
if (!file.exists()) {
int sleep = 2000;
log.info("文件{}不存在,休眠{}ms", file.getAbsolutePath(), sleep);
TimeUnit.MILLISECONDS.sleep(sleep);
continue;
}
//获取文件的上次修改时间
Long modified = file.lastModified();
//如果文件上次修改时间发生改变,使用类加载器重载文件
if (modified > lastModified.get()) {
//当前文件的文件上次修改时间
lastModified.set(modified);
log.info("文件{}存在且发生改变,lastModified:{},modified:{},开始热部署", file.getAbsolutePath(), lastModified.get(), modified);
//创建自定义文件类加载器
FileClassLoader loader = new FileClassLoader(rootDir);
//加载指定的class文件,直接调用findClass(),绕过检测机制,创建不同class对象。
Class<?> clazz = loader.findClass(classSourcePath);
//调用类的构造方法创建对象
Object obj = clazz.newInstance();
//获取该对象的方法
String methodName = "type";
Method method = clazz.getMethod(methodName, null);
//执行方法
method.invoke(obj, null);
}
//没有改变,线程休眠
else {
log.info("文件{}存在未发生改变,lastModified:{},modified:{}", file.getAbsolutePath(), lastModified.get(), modified);
TimeUnit.MILLISECONDS.sleep(5000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}