类加载运行全过程

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

  1. public class MathDemo {
  2. public static final int initData = 666;
  3. public static User user = new User();
  4. public static void main(String[] args) {
  5. Math math = new Math();
  6. math.compute();
  7. }
  8. public int compute() { // 一个方法对应一块栈帧内存区域
  9. int a = 1;
  10. int b = 2;
  11. int c = (a + b) * 10;
  12. return c;
  13. }
  14. }

通过Java命令执行代码的大体流程如下:
image.png

其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在磁盘上查找并通过IO读取字节码文件,使用到类时才会加载。例如调用类的main()方法,new 对象等,在加载阶段会在内存中生成一个代表这个类的**java.lang.Class**对象,作为方法区这个类的各种数据的访问入口。
  • 验证:校验字节码文件的正确性。
  • 准备:给类的静态变量分配内存,并赋予默认值。
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
  • 初始化:对类的静态变量初始化为指定的值,并执行静态代码块。

如下图所示:
image.png
类被加载到方法区中后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用。
对应class实例的引用:类加载在加载类信息放到方法区中后,会创建一个对应的Class类型的对象实例放到 堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点。

注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包中的类不是一次性全部加载的,使用到时才会加载。

  1. public class TestDynamicLoad {
  2. static {
  3. System.out.println("******************** load TestDynamicLoad ********************");
  4. }
  5. public static void main(String[] args) {
  6. new A();
  7. System.out.println("-----------------------------------------");
  8. B b = null;
  9. }
  10. static class A {
  11. static {
  12. System.out.println("******************** load A ********************");
  13. }
  14. public A() {
  15. System.out.println("******************** initial A ********************");
  16. }
  17. }
  18. static class B {
  19. static {
  20. System.out.println("******************** laod B ********************");
  21. }
  22. public B() {
  23. System.out.println("******************** initial B ********************");
  24. }
  25. }
  26. }

输出结果是:
image.png

类加载器和双亲委派机制

类加载过程主要是通过类记载器来实现的,Java中有如下几种类加载器

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jarcharsets.jar等。
  • 扩展类加载器:负责加载职称JVM运行的位于JRE的lib目录下的ext扩展目录中的jar类包。
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要是我们自己写的那些类。
  • 自定义加载器:负责加载用户自定义路径下的类包。

    1. public class TestJDKClassLoader {
    2. public static void main(String[] args) {
    3. System.out.println(String.class.getClassLoader());
    4. System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
    5. System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
    6. System.out.println();
    7. ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    8. ClassLoader extClassLoader = appClassLoader.getParent();
    9. ClassLoader bootstrapLoader = extClassLoader.getParent();
    10. System.out.println("the bootstrapLoader:" + bootstrapLoader);
    11. System.out.println("the extClassLoader:" + extClassLoader);
    12. System.out.println("the appClassLoader:" + appClassLoader);
    13. System.out.println();
    14. System.out.println("bootstrapLoader加载了:");
    15. URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    16. for (URL url : urLs) {
    17. System.out.println(url);
    18. }
    19. System.out.println();
    20. System.out.println("extClassLoader加载了:");
    21. System.out.println(System.getProperty("java.ext.dirs"));
    22. System.out.println("appClassLoader加载了:");
    23. System.out.println(System.getProperty("java.class.path"));
    24. }
    25. }

    运行后输出结果为: ``` 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

  1. <a name="kkgD0"></a>
  2. ## 类加载器初始化过程
  3. 参见类加载运行全过程图克制,其中会创建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**的实例加载我们的应用程序。
  4. ```java
  5. public Launcher() {
  6. ExtClassLoader var1;
  7. try {
  8. // 构造扩展类加载器,在构造的过程中将父类加载器赋值null
  9. var1 = Launcher.ExtClassLoader.getExtClassLoader();
  10. } catch (IOException var10) {
  11. throw new InternalError("Could not create extension class loader", var10);
  12. }
  13. try {
  14. this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  15. } catch (IOException var9) {
  16. throw new InternalError("Could not create application class loader", var9);
  17. }
  18. Thread.currentThread().setContextClassLoader(this.loader);
  19. String var2 = System.getProperty("java.security.manager");
  20. // 。。。。。。省略若干代码
  21. }

双亲委派机制

什么是双亲委派机制?

JVM类加载器存在亲子结构,如下图:
image.png
这里类加载中存在双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如之前的Math类,最先委托应用程序加载器(AppClassLoader)加载,应用程序类加载器会委托扩展类加载器(ExtClassLoader)加载,扩展类加载器再委托引导类加载器(BootstrapClassLoader)。顶层的引导类加载器在自己的类加载路径下无法找到Math类,则向下回退加载Math类的请求,扩展类加载器收到回复后自己尝试加载,加载不到Math类,又向下回退Math类加载请求给应用程序类加载器,应用程序类加载器找到Math,进行加载。
简单来说:先找父亲加载,找不到再由儿子自己加载
翻阅AppClassLoader加载类的双亲委派机制源码,其loadClass()方法最终会调用其父类ClassLoaderloadClass()方法,步骤大致如下:

  1. 首先检查指定名称的类是否已经加载过,如果加载过不需要重新加载,直接返回。
  2. 如果类没有加载过,首先判断是否存在父加载器,如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false)或者调用BootstrapClassLoader来加载)。
  3. 如果父加载器及引导类加载器(BootstrapClassLoader)都没有找到指定的类,那么调用当前类加载器的findClass()方法来完成类加载。

    1. // ClassLoader#loadClass方法中实现了双亲委派机制
    2. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    3. synchronized (getClassLoadingLock(name)) {
    4. // 首先,检查当前类加载器是否已经加载了改类
    5. Class<?> c = findLoadedClass(name);
    6. if (c == null) {
    7. long t0 = System.nanoTime();
    8. try {
    9. if (parent != null) { // 如果当前加载器父加载器不为空则向上委托
    10. c = parent.loadClass(name, false);
    11. } else {
    12. c = findBootstrapClassOrNull(name); // 如果当前类加载器父加载器为null,则委托引导类加载器加载
    13. }
    14. } catch (ClassNotFoundException e) {
    15. // ClassNotFoundException thrown if class not found
    16. // from the non-null parent class loader
    17. }
    18. if (c == null) {
    19. // If still not found, then invoke findClass in order
    20. // to find the class.
    21. long t1 = System.nanoTime();
    22. // 调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
    23. c = findClass(name);
    24. // this is the defining class loader; record the stats
    25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    27. sun.misc.PerfCounter.getFindClasses().increment();
    28. }
    29. }
    30. if (resolve) {
    31. resolveClass(c);
    32. }
    33. return c;
    34. }
    35. }

    为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被记在,这样可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父亲已经加载了该类是,就没有必要在子ClassLoader再加载一次,保证被加载类的唯一性

看一个类加载的实例,当我们自己在指定的包下创建String类,看看在类加载器运行时会发生什么:

  1. package java.lang;
  2. /**
  3. * @author zhenzicheng
  4. * @DESCRIPTION:
  5. * @DATE: 2022/05/14 7:04 PM
  6. */
  7. public class String {
  8. public static void main(String[] args) {
  9. System.out.println("customer String.class");
  10. }
  11. }

会成功吗?结果报错了!
image.png

全盘委托机制

“全盘负责”是指当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,否则该类所依赖以及引用的类也由这个ClassLoader载入。

自定义类加载器

自定义类加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法:

  • loadClass(String, boolean),实现了双亲委派机制。
  • findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。

接下来使用自定义类加载器实现类加载步骤:

  1. 创建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 “我是使用自定义类加载器读取的类~”; }

}

  1. 2. 在此处打开控制台输入命令`javac HelloWorld.java -encoding utf8`,将.java文件编译成字节码
  2. 2. 拷贝字节码`HelloWorld.class`文件至`E:/test/vip/zhenzicheng/demo`中。路径可以自拟,但是层级一定要与`HelloWorld`中**package**路径保持一致,前缀无所谓。
  3. ![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. 4. **删除**项目中原`HelloWorld.java``HelloWorld.class`文件,因为**双亲委派机制**当父加载器能扫描到是就不会让子类扫描!
  5. 4. 使用如下代码读取刚刚创建的`HelloWorld.class`
  6. ```java
  7. /**
  8. * 自定义类加载器
  9. *
  10. * @author zhenzicheng
  11. * @date 2022-05-14 22:57
  12. */
  13. public class MyClassLoaderTest {
  14. public static void main(String[] args) throws ReflectiveOperationException {
  15. // 初始化自定义类加载器,会先初始化父类ClassLoader,然后会将自定义类加载器的父加载器设置为应用程序加载器AppClassLoader
  16. MyClassLoader classLoader = new MyClassLoader("E:/test");
  17. Class<?> clazz = classLoader.loadClass("vip.zhenzicheng.demo.HelloWorld");
  18. // 同级目录下创建HelloWorld类
  19. Object obj = clazz.newInstance();
  20. Method method = clazz.getMethod("toString");
  21. Object result = method.invoke(obj);
  22. System.out.println(result);
  23. System.out.println(clazz.getClassLoader().getClass().getName());
  24. }
  25. static class MyClassLoader extends ClassLoader {
  26. private String classPath;
  27. public MyClassLoader(String classPath) {
  28. this.classPath = classPath;
  29. }
  30. @Override
  31. protected Class<?> findClass(String name) {
  32. try {
  33. byte[] data = loadByte(name);
  34. // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
  35. return super.defineClass(name, data, 0, data.length);
  36. } catch (IOException e) {
  37. throw new RuntimeException(e);
  38. }
  39. }
  40. private byte[] loadByte(String name) throws IOException {
  41. String fileName = name.replaceAll("\\.", "/");
  42. FileInputStream fis = new FileInputStream(classPath + "/" + fileName + ".class");
  43. int len = fis.available();
  44. byte[] data = new byte[len];
  45. fis.read(data);
  46. fis.close();
  47. return data;
  48. }
  49. }
  50. }

结果如下,使用了自定义的类加载器成功!
image.png