类加载运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
public class MathDemo {
public static final int initData = 666;
public static User user = new User();
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
public int compute() { // 一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
}
通过Java命令执行代码的大体流程如下:
其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
- 加载:在磁盘上查找并通过IO读取字节码文件,使用到类时才会加载。例如调用类的
main()
方法,new
对象等,在加载阶段会在内存中生成一个代表这个类的**java.lang.Class**
对象,作为方法区这个类的各种数据的访问入口。 - 验证:校验字节码文件的正确性。
- 准备:给类的静态变量分配内存,并赋予默认值。
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如
main()
方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。 - 初始化:对类的静态变量初始化为指定的值,并执行静态代码块。
如下图所示:
类被加载到方法区中后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用。
对应class实例的引用:类加载在加载类信息放到方法区中后,会创建一个对应的Class
类型的对象实例放到 堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。jar
包或war
包中的类不是一次性全部加载的,使用到时才会加载。
public class TestDynamicLoad {
static {
System.out.println("******************** load TestDynamicLoad ********************");
}
public static void main(String[] args) {
new A();
System.out.println("-----------------------------------------");
B b = null;
}
static class A {
static {
System.out.println("******************** load A ********************");
}
public A() {
System.out.println("******************** initial A ********************");
}
}
static class B {
static {
System.out.println("******************** laod B ********************");
}
public B() {
System.out.println("******************** initial B ********************");
}
}
}
类加载器和双亲委派机制
类加载过程主要是通过类记载器来实现的,Java中有如下几种类加载器
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如
rt.jar
、charsets.jar
等。 - 扩展类加载器:负责加载职称JVM运行的位于JRE的lib目录下的ext扩展目录中的
jar
类包。 - 应用程序类加载器:负责加载
ClassPath
路径下的类包,主要是我们自己写的那些类。 自定义加载器:负责加载用户自定义路径下的类包。
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassLoader.getParent();
System.out.println("the bootstrapLoader:" + bootstrapLoader);
System.out.println("the extClassLoader:" + extClassLoader);
System.out.println("the appClassLoader:" + appClassLoader);
System.out.println();
System.out.println("bootstrapLoader加载了:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url);
}
System.out.println();
System.out.println("extClassLoader加载了:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("appClassLoader加载了:");
System.out.println(System.getProperty("java.class.path"));
}
}
运行后输出结果为: ``` null sun.misc.Launcher$ExtClassLoader sun.misc.Launcher$AppClassLoader
the bootstrapLoader:null the extClassLoader:sun.misc.Launcher$ExtClassLoader@4b67cf4d the appClassLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
bootstrapLoader加载了: file:/E:/Env/Java/jdk1.8.0_202/jre/lib/resources.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/rt.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/jsse.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/jce.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/charsets.jar file:/E:/Env/Java/jdk1.8.0_202/jre/lib/jfr.jar file:/E:/Env/Java/jdk1.8.0_202/jre/classes
extClassLoader加载了: E:\Env\Java\jdk1.8.0_202\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext appClassLoader加载了: E:\Env\Java\jdk1.8.0_202\jre\lib\charsets.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\deploy.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\javaws.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\jce.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\jfr.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\jsse.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\management-agent.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\plugin.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\resources.jar;E:\Env\Java\jdk1.8.0_202\jre\lib\rt.jar;E:\codes\Gitee Repository\study-codes\java-parent\target\classes;E:\Tools\IntelliJ IDEA 2022.1\lib\idea_rt.jar
<a name="kkgD0"></a>
## 类加载器初始化过程
参见类加载运行全过程图克制,其中会创建JVM启动器实例`sun.misc.Launcher`。`sun.misc.Launcher`初始化使用了单例模式,保证一个JVM虚拟机内只有一个`sun.misc.Launcher`实例。<br />在`Launcher`构造方法内部,创建了两个类加载器,分别是`sun.misc.Launcher.ExtClassLoader`(扩展类加载器)和`sun.misc.Launcher.AppClassLoader`(应用类加载器)。<br />JVM默认使用**Launcher**的`getClassLoader()`方法返回的类加载器**AppClassLoader**的实例加载我们的应用程序。
```java
public Launcher() {
ExtClassLoader var1;
try {
// 构造扩展类加载器,在构造的过程中将父类加载器赋值null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
// 。。。。。。省略若干代码
}
双亲委派机制
什么是双亲委派机制?
JVM类加载器存在亲子结构,如下图:
这里类加载中存在双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如之前的Math
类,最先委托应用程序加载器(AppClassLoader)加载,应用程序类加载器会委托扩展类加载器(ExtClassLoader)加载,扩展类加载器再委托引导类加载器(BootstrapClassLoader)。顶层的引导类加载器在自己的类加载路径下无法找到Math类,则向下回退加载Math类的请求,扩展类加载器收到回复后自己尝试加载,加载不到Math类,又向下回退Math类加载请求给应用程序类加载器,应用程序类加载器找到Math,进行加载。
简单来说:先找父亲加载,找不到再由儿子自己加载。
翻阅AppClassLoader加载类的双亲委派机制源码,其loadClass()
方法最终会调用其父类ClassLoader的loadClass()
方法,步骤大致如下:
- 首先检查指定名称的类是否已经加载过,如果加载过不需要重新加载,直接返回。
- 如果类没有加载过,首先判断是否存在父加载器,如果有父加载器,则由父加载器加载(即调用
parent.loadClass(name, false)
或者调用BootstrapClassLoader来加载)。 如果父加载器及引导类加载器(BootstrapClassLoader)都没有找到指定的类,那么调用当前类加载器的
findClass()
方法来完成类加载。// ClassLoader#loadClass方法中实现了双亲委派机制
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); // 如果当前类加载器父加载器为null,则委托引导类加载器加载
}
} 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.
long t1 = System.nanoTime();
// 调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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.lang.String.class
类不会被记在,这样可以防止核心API库被随意篡改。 - 避免类的重复加载:当父亲已经加载了该类是,就没有必要在子ClassLoader再加载一次,保证被加载类的唯一性。
看一个类加载的实例,当我们自己在指定的包下创建String
类,看看在类加载器运行时会发生什么:
package java.lang;
/**
* @author zhenzicheng
* @DESCRIPTION:
* @DATE: 2022/05/14 7:04 PM
*/
public class String {
public static void main(String[] args) {
System.out.println("customer String.class");
}
}
全盘委托机制
“全盘负责”是指当一个ClassLoader
加载一个类时,除非显示的使用另一个ClassLoader
,否则该类所依赖以及引用的类也由这个ClassLoader
载入。
自定义类加载器
自定义类加载器只需要继承java.lang.ClassLoader
类,该类有两个核心方法:
loadClass(String, boolean)
,实现了双亲委派机制。findClass
,默认实现是空方法,所以我们自定义类加载器主要是重写findClass
方法。
接下来使用自定义类加载器实现类加载步骤:
- 创建
HelloWorld.java
,内容如下 ```java package vip.zhenzicheng.demo;
/**
- @author zhenzicheng
- @DESCRIPTION: 测试自定义类加载器读取demo
- @DATE: 2022/05/14 9:57 PM */ public class HelloWorld { @Override public String toString() { return “我是使用自定义类加载器读取的类~”; }
}
2. 在此处打开控制台输入命令`javac HelloWorld.java -encoding utf8`,将.java文件编译成字节码
2. 拷贝字节码`HelloWorld.class`文件至`E:/test/vip/zhenzicheng/demo`中。路径可以自拟,但是层级一定要与`HelloWorld`中**package**路径保持一致,前缀无所谓。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/22484004/1652539487508-fd280ed6-b32e-4260-a5a7-b2d1e516b9d6.png#clientId=u2964d5b8-4a33-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=127&id=u378de076&margin=%5Bobject%20Object%5D&name=image.png&originHeight=153&originWidth=871&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23228&status=done&style=none&taskId=ud5a7d960-0194-49f4-804e-c35209d473b&title=&width=725.5333557128906)
4. **删除**项目中原`HelloWorld.java`和`HelloWorld.class`文件,因为**双亲委派机制**当父加载器能扫描到是就不会让子类扫描!
4. 使用如下代码读取刚刚创建的`HelloWorld.class`:
```java
/**
* 自定义类加载器
*
* @author zhenzicheng
* @date 2022-05-14 22:57
*/
public class MyClassLoaderTest {
public static void main(String[] args) throws ReflectiveOperationException {
// 初始化自定义类加载器,会先初始化父类ClassLoader,然后会将自定义类加载器的父加载器设置为应用程序加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("E:/test");
Class<?> clazz = classLoader.loadClass("vip.zhenzicheng.demo.HelloWorld");
// 同级目录下创建HelloWorld类
Object obj = clazz.newInstance();
Method method = clazz.getMethod("toString");
Object result = method.invoke(obj);
System.out.println(result);
System.out.println(clazz.getClassLoader().getClass().getName());
}
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) {
try {
byte[] data = loadByte(name);
// defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return super.defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private byte[] loadByte(String name) throws IOException {
String fileName = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + fileName + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
}
结果如下,使用了自定义的类加载器成功!