目录
一、java类加载过程
1.1、类加载过程
当我们用java命令运行某个类的main函数启动程序时,首先是由java.exe调用jvm.dll创建Java虚拟机,再由其虚拟机创建一个引导类加载器实例,调用java代码创建jvm启动器实例sun.misc.Launcher,获取运行类Launcher相应类加载器后,加载执行的类资源后调用其main执行的入口方法,执行相关的业务逻辑代码后,Java程序运行结束,JVM就销毁,相大致流程如下:
其中loadClass的类加载过程有如下几步:加载 >> 验证 >> 准备 >> 解析 >> 初始化
1、加载:从磁盘上读取类的class文件;
2、验证:主要是验证读取的class文件的格式是否是正确的;
3、准备:将定义好的静态变量设置默认值,分配内存,如静态变量int型赋值0,静态变量布尔型
赋值false等;
4、解析:将符号引用转变为直接引用,直接引用中分为静态连接和动态连接;
5、初始化:对类的静态变量初始化为指定的值,执行静态代码块;
类被加载到方法区中后主要包含运行时常量池、类信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中,如下图math实例, 作为开发人员访问方法区中类定义的入口和切入点。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。
jar包或war包里的类不是一次性全部加载的,是使用到时才加载
package com.jvm.zm01;
/**
* 主类在运行过程中如果使用到其它类,会逐步加载这些类。
* jar包或war包里的类不是一次性全部加载的,是使用到时才加载
*@Author : hongzm
*@Date: 2023-09-23 12:54
*@Version: 1.0
*/
public class TestDynamicLoad {
static {
System.out.println("*********************load TestDynamicLoad*******************");
}
public static void main(String[] args) {
new A();
System.out.println("**************load tset****************");
B b = null; // 这里b不会加载,除非这里执行new B()
}
}
/**
* 一定是先加载(如static 静态语句块)然后才初始化(实例化,如 new A())
*/
class A{
static {
System.out.println("**************load A****************");
}
public A(){
System.out.println("**************init A****************");
}
}
class B{
static {
System.out.println("**************load B****************");
}
public B(){
System.out.println("**************init B****************");
}
}
运行结果
*********************load TestDynamicLoad*******************
**************load A****************
**************init A****************
**************load tset****************
二、类加载器和双亲委派机制
2.1、类加载器分类
java中的类加载器主要可以分为四类:引导类加载器、扩展类加载器、应用类加载器、自定义加载器;
1、引导类加载器BootstrapLoader(C++实现):
负责加载支撑JVM运行的位于JRE的lib目录下核心类,如rt.jar、charsets.jar等;
2、扩展类加载器ExtClassLoader
负责加载支撑JVM运行的位于JRE的lib目录下的ext拓展目录中jar类包;
3、应用程序类加载器AppClassLoader
负责加载ClassPath路径下类包(自己写的类:target目录下)
4、自定义加载器
负责加载用户自定义路径下的类;
package com.jvm.zm02;
import com.sun.crypto.provider.DESKeyFactory;
import sun.misc.Launcher;
import java.net.URL;
/**
*
*@Author : hongzm
*@Date: 2023-09-23 13:03
*@Version: 1.0
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());//rt.jar包下 输出null,因为引导类加载器是用c++实现,在java环境取不到信息
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());// jre/lib/ext包下 输出sun.misc.Launcher$ExtClassLoader
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());//用户自定义 输出sun.misc.Launcher$AppClassLoader
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");// 引导类加载加载的
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
System.out.println();
System.out.println("extClassLoader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
执行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/classes
extClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext
appClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_201\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar;
C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.3\lib\idea_rt.jar
Process finished with exit code 0
2.2、类加载器的初始化过程
创建JVM启动器实例sun.misc.Launcher。在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
1)构造扩展类加载器var1 = Launcher.ExtClassLoader.getExtClassLoader();,在构造的过程中将其父加载器设置为null;
2)构造应用类加载器this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);,在构造的过程中将其父加载器设置为ExtClassLoader;
2.3、双亲委派机制
1、先看一个类加载到JVM的过程是一个什么样的
接2.2小结所说,JVM默认使用Launcher的getClassLoader()方法返回的类加载器为:AppClassLoader实例,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,大体流程如下:
首先,会检查指定的类是否已经加载过了,如果加载过了,直接返回,底层的逻辑代码如下:
如果这个类没有被加载过,就先判断其是否有父加载类,如果有,就调父加载类进行加载,如果没有,就调用bootstrap加载类加载,图示如下,
而AppClassLoader父加载器是ExtClassLoader,
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载,如下图示:
那我们再回到最初的问题,加载一个类的过程,首先是会先判断该类在已加载的列表中是否存在,存在直接返回,不存在就先判断是否有父加载类,有的话,委托给父加载器加载,以此类推,如果引导类bootstrap加载到不到目标类,则向下退回加载类的请求,由子类自己加载,以此逐级退回,这样一个过程,我们就称之为类加载的双亲委派机制,简单的说就是先找父亲加载,加载不到再由儿子自己加载。
2、那为什么要设计双亲委派机制呢
主要有以下两方面:
沙箱安全机制:主要是能够保护java核心类库资源不会被其他新写的同包同名的类给覆盖掉,这样子可以防止核心的API库被篡改;
类不被重复加载:当父类已经加载过该类了,那就没有必要让子加载类再重新加载一遍,这可以保证加载类的唯一性;
示例验证:
用户编写一个java.lang.String类,里面有一个main(String[] args)方法,执行main方法;
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
分析:
由双亲委派机制可知,点击执行main方法后,在加载类过程中,应用程序类加载器向上委托拓展类加载器,拓展类加载器再向上委托引导类加载器,引导类加载器加载JRE的lib目录下核心类,
加载到包含路径为java.lang.String的类(没有main方法),就直接返回了,不会再去加载用户编写的同名类(沙箱安全机制),故报找不到main方法报错。
三、自定义类加载器与打破双亲委派机制
3.1、自定义类加载器
java.lang.ClassLoader 类有两个核心方法:loadClass(实现双亲委派逻辑),findClass(默认实现是空方法);
所以我们自定义一个类加载器只需要两步:
1)继承java.lang.ClassLoader 类
2)重写findClass方法:默认实现是空方法
3.2、自定义类加载器示例
示例1:
将工程目录下的User.class文件拷贝到指定目录下,如D:\test\com\jvm\zm01,测试自定义类加载器加载class文件
/**
* 自定义类加载器
*@Author : hongzm
*@Date: 2023-09-23 13:32
*@Version: 1.0
*/
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.jvm.zm01.User");
System.out.println(clazz.getClassLoader());
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
再次印证双亲委派机制,父加载器AppClassLoader先加载了User.class后,自定义加载器就不会重复加载
示例2:
删除项目里面的User.class文件,再测试:
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.jvm.zm01.User");
System.out.println(clazz.getClassLoader());
}
运行结果:
com.jvm.zm02.MyClassLoaderTest$MyClassLoader@4554617c
3.3、那么如何打破双亲委派呢?
打破双亲委派机制也就是不委托父类去加载自定义的类,必须再重写loadClass方法(打破双亲委派),也就是项目User.class文件不删除,实现用自定义类加载器加载我们自己的User.class,
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @return
* @throws ClassNotFoundException
*/
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//非自定义的类还是走双亲委派加载
if (!name.startsWith("com.jvm")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 打破双亲委派
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
//尝试用自己改写类加载机制去加载自己写的java.lang.String.class
Class clazz = classLoader.loadClass("com.jvm.zm01.User");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果
**********自定义类加载器加载类调用方法***********
com.jvm.zm02.MyClassLoaderTest$MyClassLoader