关于从JDK源码剖析JVM类加载机制

一、Java指令运行程序流程

  1. 首先通过类加载器将字节码文件加载到 JVM 虚拟机中,以自定义 Math 类为例:java com.autumn.Math.class
public class Math {
	public static int num = 123;
    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
    	Math math = new Math();
        math.compute();
    }
}
  1. 以 Windows 系统为例,java 指令即为 C++ 语言编写的 java.exe,它会调用底层 C++ 的库函数 jvm.dll 文件来创建 Java 的虚拟机
  2. 虚拟机会创建一个引导类加载器实例
  3. C++ 接着会调用 Java 代码创建 JVM 启动程序,其中的 sun.misc.Launcher 会由引导类加载器负责加载创建很多 Java 实现的类加载器:sun.misc.Launcher.getLauncher()
  4. 再获取运行类自己的加载器 ClassLoader,Math 类的加载器获取结果为 AppClassLoader:launcher.getClassLoader()
  5. 调用 loadClass 方法来加载磁盘上的字节码文件 Math 类:classLoader.loadClass(“com.autumn.Math.class”)
  6. 加载完成后 C++ 调用 JVM 执行 Math 类的 main 方法:Math.main()
  7. 程序运行结束,JVM 销毁
    在这里插入图片描述

二、loadClass类加载过程

  1. 加载:加载磁盘里的字节码文件,即 target 目录下的文件,使用到对应的类时才会加载,例如调用 main 方法,创建对象等,在内存中生成这个类的 java.lang.Class 对象,作为方法区此类各种数据的访问入口

字节码文件默认开头:cafe babe

  1. 验证:验证字节码文件是否格式正确
  2. 准备:为静态变量做初始值赋值

例如:int 初始值为 0,boolean 初始值为 false,对象为 null

  1. 解析:将符号引用替换为直接引用,main 方法和一些静态方法替换为指向数据所存内存的指针或句柄,即为静态链接的过程,在类加载期间完成

符号:指代码里的修饰符、返回值、方法名、参数、类名等
静态链接:当这些符号加载到内存时所对应的地址
动态链接:加载时候不一定会解析成内存地址,直到运行到的时候才解析

将字节码文件分解成可读性更好的格式的指令:javap -v Math.class
生成的文件里面#数字所对应的字段即为符号

Classfile /D:/SelfProject/demo/target/classes/com/autumn/jvm/Math.class
    Last modified 2022-10-1; size 753 bytes
    MD5 checksum 7fa2b091a0b6bbf85f53b67854dca927
    Compiled from "Math.java"
public class com.autumn.jvm.Math
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref          #9.#34         // java/lang/Object."<init>":()V
    #2 = Class              #35            // com/autumn/jvm/Math
    #3 = Methodref          #2.#34         // com/autumn/jvm/Math."<init>":()V
    #4 = Methodref          #2.#36         // com/autumn/jvm/Math.compute:()I
    #5 = Fieldref           #2.#37         // com/autumn/jvm/Math.num:I
    #6 = Class              #38            // com/autumn/jvm/User
    #7 = Methodref          #6.#34         // com/autumn/jvm/User."<init>":()V
    #8 = Fieldref           #2.#39         // com/autumn/jvm/Math.user:Lcom/autumn/jvm/User;
    #9 = Class              #40            // java/lang/Object
    #10 = Utf8               num
    #11 = Utf8               I
    #12 = Utf8               user
    #13 = Utf8               Lcom/autumn/jvm/User;
	#14 = Utf8               <init>
    #15 = Utf8               ()V
    #16 = Utf8               Code
    #17 = Utf8               LineNumberTable
    #18 = Utf8               LocalVariableTable
    #19 = Utf8               this
    #20 = Utf8               Lcom/autumn/jvm/Math;
	#21 = Utf8               compute
    #22 = Utf8               ()I
    #23 = Utf8               a
    #24 = Utf8               b
    #25 = Utf8               c
    #26 = Utf8               main
    #27 = Utf8               ([Ljava/lang/String;)V
    #28 = Utf8               args
    #29 = Utf8               [Ljava/lang/String;
	#30 = Utf8               math
    #31 = Utf8               <clinit>
    #32 = Utf8               SourceFile
    #33 = Utf8               Math.java
    #34 = NameAndType        #14:#15        // "<init>":()V
    #35 = Utf8               com/autumn/jvm/Math
    #36 = NameAndType        #21:#22        // compute:()I
    #37 = NameAndType        #10:#11        // num:I
    #38 = Utf8               com/autumn/jvm/User
    #39 = NameAndType        #12:#13        // user:Lcom/autumn/jvm/User;
    #40 = Utf8               java/lang/Object
{
    public static int num;
        descriptor: I
        flags: ACC_PUBLIC, ACC_STATIC

    public static com.autumn.jvm.User user;
        descriptor: Lcom/autumn/jvm/User;
        flags: ACC_PUBLIC, ACC_STATIC

    public com.autumn.jvm.Math();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
            stack=1, locals=1, args_size=1
                0: aload_0
                1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                4: return
            LineNumberTable:
            	line 9: 0
            LocalVariableTable:
            	Start  Length  Slot  Name   Signature
            		0       5     0  this   Lcom/autumn/jvm/Math;

    public int compute();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
            stack=2, locals=4, args_size=1
            0: iconst_1
            1: istore_1
            2: iconst_2
            3: istore_2
            4: iload_1
            5: iload_2
            6: iadd
            7: bipush        10
            9: imul
            10: istore_3
            11: iload_3
            12: ireturn
        LineNumberTable:
            line 14: 0
            line 15: 2
            line 16: 4
            line 17: 11
        LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      13     0  this   Lcom/autumn/jvm/Math;
                2      11     1     a   I
                4       9     2     b   I
                11       2     3     c   I

    public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=2, args_size=1
            0: new           #2                  // class com/autumn/jvm/Math
            3: dup
            4: invokespecial #3                  // Method "<init>":()V
            7: astore_1
            8: aload_1
            9: invokevirtual #4                  // Method compute:()I
            12: pop
            13: return
        LineNumberTable:
            line 21: 0
            line 22: 8
            line 23: 13
        LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      14     0  args   [Ljava/lang/String;
                8       6     1  math   Lcom/autumn/jvm/Math;

    static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
            stack=2, locals=0, args_size=0
            0: bipush        123
            2: putstatic     #5                  // Field num:I
            5: new           #6                  // class com/autumn/jvm/User
            8: dup
            9: invokespecial #7                  // Method com/autumn/jvm/User."<init>":()V
            12: putstatic     #8                  // Field user:Lcom/autumn/jvm/User;
            15: return
        LineNumberTable:
            line 10: 0
            line 11: 5
}
SourceFile: "Math.java"
  1. 初始化:将静态变量初始化为指定的值,以及执行静态代码块
  2. 使用
  3. 卸载
    在这里插入图片描述

类被加载到方法区后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。

类加载器的引用:这个类到类加载器实例的引用
对应 class 实例的引用:类加载器在加载类信息到方法区后,会创建一个对应的 Class 类型的对象实例(java.lang.Class对象),放到堆中,作为开发人员访问方法区中类定义的入口和切入点

类加载属于懒加载,主类在运行过程中若使用到其它类,才逐步加载这些类。
因此 jar 包或 war 包里的类并非一次性加载,而是使用时才会加载。

加载顺序:主类的静态代码块,main 方法里按顺序执行到便加载,若 new 对象则加载,若对象声明为 null 不加载

三、类加载器和双亲委派机制

上述类加载过程均由类加载器实现

  1. 引导类加载器 BootstrapClassLoader:加载支撑 JVM 运行的位于 jre 的 lib 目录下的核心类库,例如 rt.jar、charset.jar 等

String.class.getClassLoader()
String 属于核心类库,引导类均为 C++ 生成的对象,所以无法用 Java 获取,结果为 null

  1. 扩展类加载器 ExtClassLoader:加载支撑 JVM 运行的位于 jre 的 lib 目录下的 ext 扩展目录中的 jar 包

com.sun.crypto.provider.DESKeyFactory.class.getClassLoader()
结果为 sun.misc.Launcher$ExtClassLoader@xxxxxxxx

  1. 应用程序类加载器 AppClassLoader:加载 ClassPath 路径下的类包,即自己写的类

TestJDKClassLoader.class.getClassLoader()
此类为自己写的类,结果为 sun.misc.Launcher$AppClassLoader@xxxxxxxx

  1. 自定义加载器:加载用户自定义路径下的类包

Launcher 类为 rt.jar 包里最核心的类,继承 URLClassLoader。
Launcher 类初始化过程,调用 getLacuncher():
返回 launcher,launcher 在静态变量里 new Launcher(),即为单例对象;
Launcher 类构造方法:
①定义一个扩展类加载器:Launcher.ExtClassLoader
②调用 getExtClassLoader 方法来获取类加载器
扩展类加载器的初始化 getExtClassLoader:
经过校验,返回 new Launcher.ExtClassLoader(),即初始化一个扩展类加载器;
初始化时会调用父类加载器 URLClassLoader,根据传入的磁盘路径,进行文件读写,加载到内存中。
③调用应用程序类加载器的 getAppClassLoader 方法,将扩展类加载器作为参数
先获取环境变量:System.getProperty(“java.class.path”),经过一些操作,最终返回 new Launcher.AppClassLoader(),即初始化一个应用程序类加载器;
初始化的 AppClassLoader 会调用父构造方法,即 URLClassLoader,作用同上。

ClassLoader.getSyetemClassLoader()即为appClassLoader:系统类生成的类加载器,为应用程序类加载器

sun.misc.Launcher$AppClassLoader@xxxxxxxx
加载了环境变量下类路径里的包:System.getProperty(“java.class.path”)
虽然 appClassLoader 会扫描类路径里所有的包,但只会去加载 target 目录下的包

appClassLoader.getParent()即为extClassLoader:应用程序类加载器的上级加载器为扩展类加载器

sun.misc.Launcher$ExtClassLoader@xxxxxxxx
加载了环境变量下的 ext 目录:System.getProperty(“java.ext.dirs”)

extClassLoader.getParent()即为bootstrapLoader:扩展类加载器的上级加载器为引导类加载器

结果为 null,实际上加载了引导类加载器的内路径包:Launcher.getBootstrapClassPath().getURLs(),结果为 URL 数组

System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println("----------------------------------------");

ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("the bootstrapClassLoader >>> " + bootstrapClassLoader);
System.out.println("the extClassLoader >>> " + extClassLoader);
System.out.println("the appClassLoader >>> " + appClassLoader);
System.out.println("----------------------------------------");

System.out.println("bootstrapClassLoader 加载以下文件:");
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("----------------------------------------");

System.out.println("appClassLoader 加载了以下文件:");
System.out.println(System.getProperty("java.class.path"));

//运行结果如下
/* 
null
sun.misc.Launcher$ExtClassLoader@33c7353a
sun.misc.Launcher$AppClassLoader@18b4aac2
----------------------------------------
the bootstrapClassLoader >>> null
the extClassLoader >>> sun.misc.Launcher$ExtClassLoader@33c7353a
the appClassLoader >>> sun.misc.Launcher$AppClassLoader@18b4aac2
----------------------------------------
bootstrapClassLoader 加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/classes
----------------------------------------
extClassLoader 加载了以下文件:
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
----------------------------------------
appClassLoader 加载了以下文件:
C:\Program Files\Java\jdk1.8.0_301\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\management- agent.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\rt.jar;D:\SelfProject\demo\target\classes;
D:\.m2\repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;
D:\.m2\repository\com\google\guava\guava\21.0\guava-21.0.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.3\lib\idea_rt.jar
*/
类加载器初始化过程

先创建 JVM 启动器实例 sun.misc.Launcher:Launcher 初始化为单例模式,保证一个 JVM 仅有一个 Launcher 实例;
Launcher 构造方法内,分别创建了两个类加载器:sun.misc.Launcher.ExtClassLoader 扩展类加载器、sun.misc.Launcher.AppClassLoader 应用程序类加载器;
JVM 默认使用 Launcher.getClassLoader() 返回的 AppClassLoader 的实例加载自己写的应用程序。

public Launcher() {
	Launcher.ExtClassLoader var1;
    try{
        //创建扩展类加载器,并将上级加载器置为 null
    	var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
    	throw new InternalError("Could not create extension class loader", var10);
    }

	try{
        //创建应用程序类加载器,并将上级加载器置为 ExtClassLoader
        //Launcher的loader 属性值是 AppClassLoader,一般用此类加载器来加载自己写的应用程序
		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");
    //以下省略 ……
}
双亲委派机制

在这里插入图片描述

当运行类进行加载时,会先通过应用程序类加载器,若应用程序类加载器已加载的包中已包含此类,则会直接加载返回,若不存在则向上委托给扩展类加载器;
扩展类加载器已加载的包中若存在此类则加载返回,若不存在则再向上委托给引导类加载器;
引导类加载器已加载的包中若存在此类则加载返回,若不存在则引导类加载器会尝试去内路径包内寻找,若仍旧寻找不到,则向下委托给扩展类加载器;
扩展类加载器则会尝试去环境变量下 ext 目录下寻找,若仍旧寻找不到,则再向下委托给应用程序类加载器,应用程序类加载器则会尝试去环境变量下 target 目录下寻找,即 loadClass 方法,找到后即加载返回。
上下级类加载器非继承关系,根据 parent 属性来进行委托,继承自 URLClassLoader,调用的是 URLClassLoader 的 loadClass 方法,URLClassLoader 再调用父类的 loadClass 方法,直到调用到根父类 ClassLoader 的 loadClass 方法。

绝大多数类都是属于程序类加载器进行加载的,虽然第一次加载步骤繁琐,但之后再次加载时,便能在第一轮程序类加载器中直接加载返回,从而提高加载性能

LoadClass 逻辑:
  1. 先检查指定名称的类是否已被当前类加载器加载过,若加载过则直接返回;
  2. 若未加载过,则判断上级类加载器是否存在,若存在则委托上级类加载器去加载,即调用 parent.loadClass(name, false);
  3. 若不存在则说明已经是最上级类加载器,即引导类加载器 bootstrapClassLoader,进行加载;
  4. 若上级类加载器与引导类加载器都找不到该类,则调用当前类的 findClass 方法来完成加载。
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {// 例如name为"com.autumn.jvm.Math"
        // 当前类加载器会先检查是否已加载过该类,最终会调用到 C++ 写的本地方法来执行
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 若当前加载器的上级不为空,则委托上级加载器加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //若为空则说明到顶层类加载器了,即引导类加载器,委托其加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } 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;
    }
}
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}
设计双亲委派机制用意:

1. 沙箱安全机制:
自己写的类若与自带的类重名,例如 java.lang.String.class,此时该类不会被加载,以此来防止核心 API 库被任意篡改。
2. 避免类的重复加载:
当下级类加载器已加载指定类时,就不会让上级类加载器再加载一次,以此保证被加载的类的唯一性。

public class String {
    public static void main(String[] args) {
        System.out.println("====== This is my String ======");
    }
}

运行时报如下错误:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application

全盘负责委托机制:

当一个类加载器加载一个指定类时,除非该类显式地使用另一个类加载器,否则该类所有的依赖和引用都仅用此类加载器加载。

四、自定义类加载器:

  1. 需继承 ClassLoader;
  2. 两大核心方法:
    ①loadClass(String, boolean),实现了双亲委派机制
    findClass(String),默认实现为空方法,需进行重写
public class MyClassLoaderTest {
    static 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;
        }

        @Override
        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 {
            // 初始化自定义类加载器,先初始化上级加载器 ClassLoader,
            // 期间将自定义类加载器的上级类加载器设置为应用程序类加载器 AppClassLoader
            MyClassLoader myClassLoader = new MyClassLoader("D:/demo");
            // D盘创建多级目录:demo/com/autumn/jvm,将 User1 类放入该目录
            Class clazz = myClassLoader.loadClass("com.autumn.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());

        }
    }
}
package com.autumn.jvm;

public class User {
    private int id;
    private String name;

    static {
        System.out.println("====== load User ======");
    }

    public User() {
    }

    public User(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void sout(){
        System.out.println("自定义加载器加载类调用方法");
    }
}

当项目中有 User1.class 时,说明应用程序类加载器 AppClassLoader 已经加载过此类了,所以在加载 D 盘的 User1.class 时,可在 AppClassLoader 找到,并直接返回,因此运行结果如下:

====== load User ======
------ 自定义加载器加载类调用方法 ------
sun.misc.Launcher$AppClassLoader

当项目中没有 User1.class 时,说明 AppClassLoader 没有加载过此类,上级类加载器也更不会有,因此最终又会向下委托,直到自定义加载器自行加载,因此运行结果如下:

====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader

五、打破双亲委派机制

用自定义类加载器加载指定的类,当项目存在该类时也不委托给应用程序类加载器,因此要重写 loadClass 方法:

@Override
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) {
            long t0 = System.nanoTime();
            // 去掉此处的向上委托,即可打破双亲委派
			/*try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } 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();

                // 加载时需加载核心类 Object,而核心类只能由引导类加载器来加载
                // 当要加载的类不是自己写的类时,向上委托,否则直接加载,即完成了打破双亲委派
                if (!name.startsWith("com.autumn.jvm")) {
                    c = this.getParent().loadClass(name);
                } else {
                    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;
    }
}

执行结果:

====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader

六、Tomcat打破双亲委派机制

Tomcat 是个 web 容器,需要解决以下几个问题:
  1. 一个 web 容器可能需部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,需要保证每个应用程序的类库互相独立、互相隔离。
  2. 部署在同一个 web 容器中相同的类库相同的版本可以共享,否则若服务器有多个应用程序还要多个相同类库加载进虚拟机。
  3. web 容器也有自己依赖的类库,不能与应用程序的类库混淆。将容器的类库和应用程序的类库隔离开来保证安全。
  4. web 容器需支持 jsp 的修改,因为 jsp 文件最终也是编译成 class 文件加载进虚拟机中运行,但程序运行后修改 jsp 是司空见惯的事,因此 web 容器需支持 jsp 修改后不用重启。
Tomcat 若使用默认的双亲委派机制无法解决上述问题:
  1. 双亲委派无法加载两个相同类库的不同版本,默认的类加载器不论什么版本,只保存一份全限定类名。
  2. 默认的类加载器可以实现同一个 web 容器中共享相同的类库相同的版本,其职责就是确保唯一性
  3. 原因同 1,无法解决第 3 个问题。
  4. 若要 web 容器支持 jsp 修改后不重启,就需实现 jsp 文件的热加载,即每个 jsp 文件对应一个唯一的类加载器,当一个 jsp 文件修改了,就直接卸载该 jsp 的类加载器,重新创建新的类加载器,加载 jsp 文件。
Tomcat 自定义加载器详解:

在这里插入图片描述

Tomcat 核心的类加载器:
  1. CommonClassLoader:Tomcat 最基本的类加载器,加载路径中的 class 可以被 Tomcat 容器本身及各个 Webapp 访问。
  2. CatalinaClassLoader:Tomcat 容器私有的类加载器,加载路径中的 class 对 Webapp 不可见。
  3. SharedClassLoader:各个 WebApp 共享的类加载器,加载路径中的 class 对所有 Webapp 可见,但对 Tomcat 容器本身不可见。
  4. WebAppClassLoader:各个 WebApp 私有的类加载器,加载路径中的 class 只对当前 WebApp 可见,比如加载 war 包里相关的类,每个 war 包应用都有各自独立的 WebAppClassLoader,相互隔离。

当不同 war 包引入不同的 spring 版本时,即可实现各自加载各自的 spring 版本。

Tomcat 几个类加载器的委派关系:
  1. CommonClassLoader 能加载的类都可以被 CatalinaClassLoader和SharedClassLoader 使用,从而实现公有类库的共用,而 CatalinaClassLoader 和 SharedClassLoader 自己加载的类则互相隔离。
  2. WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 的实例互相隔离。
  3. JasperLoader 的加载范围仅限于此 jsp 文件所编译出来的 class 文件,当 Web 容器检测到 jsp 文件被修改时,会立即丢弃目前的 JasperLoader 实例,重新创建一个新的进行加载,以此来实现 jsp 文件的热加载功能。

Tomcat 每个 WebAppClassLoader 加载自己目录下的 class 文件,不会传递给上级类加载器,打破双亲委派机制。

模拟实现 Tomcat 的 WebAppClassLoader 加载自己 war 包应用内不同版本类实现互相隔离共存的效果:

复制一份 User1,将 sout 方法里的打印内容做区分,放在 demo1 目录下,在前面已经打破双亲委派机制的自定义类加载器中创建新的实例加载 demo1 目录下的同一个类名

public static void main(String[] args) throws Exception {
    MyClassLoader myClassLoader = new MyClassLoader("D:/demo");
    Class clazz = myClassLoader.loadClass("com.autumn.jvm.User1");
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("sout", null);
    method.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName());

    MyClassLoader myClassLoader1 = new MyClassLoader("D:/demo1");
    Class clazz1 = myClassLoader1.loadClass("com.autumn.jvm.User1");
    Object obj1 = clazz1.newInstance();
    Method method1 = clazz1.getDeclaredMethod("sout", null);
    method1.invoke(obj1, null);
    System.out.println(clazz1.getClassLoader().getClass().getName());
}

执行结果:

====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader
====== load User ======
------ 另一个版本的User1:自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader

同一个 JVM 内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样。 因此要判断是否为同一个类对象的话,除了看类的包名和类名是否相同外,还需要看他们的类加载器是否为同一个。

<think>嗯,用户想找HAL_TIM_PeriodElapsedCallback函数的定义位置和用法。首先,我需要回忆一下STM32的HAL库结构。这个回调函数应该和定时器中断相关,对吧?当定时器的周期到了,就会触发这个回调函数。 根据用户提供的引用资料,特别是引用[3],里面提到使用HAL_TIM_Base_Start_IT来启动定时器并开启中断,然后实现中断服务程序中的任务。所以HAL_TIM_PeriodElapsedCallback应该是在用户代码中定义的,用来处理周期性的中断事件。HAL库通常采用回调机制,用户需要自己实现这些回调函数,比如这个函数可能是在定时器更新事件时被调用。 接下来,关于定义的位置,HAL库中的弱定义应该在某个头文件或源文件中,比如stm32xx_hal_tim.h或者对应的.c文件中。用户需要在自己的代码中重新定义这个函数,覆盖弱符号。例如,在main.c或者专门的定时器处理文件中定义。 用法方面,用户需要在初始化定时器并启动中断后,编写这个回调函数的内容,比如翻转LED或者处理数据。同时要注意定时器实例的判断,如果有多个定时器的话,需要用htim的参数来选择正确的定时器。 可能用户遇到的问题是没有正确实现回调函数,或者没有启动中断。需要检查是否调用了正确的启动函数,比如HAL_TIM_Base_Start_IT而不是HAL_TIM_Base_Start,后者可能不会开启中断。 另外,用户可能需要知道如何查找HAL库中的弱定义,这通常是在HAL的源文件中,使用__weak关键字修饰。例如在stm32xx_hal_tim.c中会有这个函数的弱定义,用户在自己的代码中重新实现即可。 还需要提醒用户,中断服务函数的名字可能和CubeMX生成的有关,确保中断向量表正确,以及中断优先级设置合理。如果有多个定时器,回调函数中需要通过htim->Instance来判断是哪个定时器触发的,从而执行不同的操作。 总结一下,回答应该包括回调函数的定义位置在用户代码中,需要覆盖HAL的弱定义,用法是在定时器中断发生时执行用户代码,并注意定时器实例的判断。同时提供示例代码和常见问题解答,帮助用户正确使用。</think>### HAL_TIM_PeriodElapsedCallback 函数解析 #### 1. 函数定义位置 `HAL_TIM_PeriodElapsedCallback` 是STM32 HAL库中定时器更新事件(周期中断)的**弱定义回调函数**,其默认实现位于HAL库的定时器驱动文件中(例如:`stm32xx_hal_tim.c`),但用户需要在工程中**重新实现该函数**以覆盖默认的弱定义[^3]。 具体来说: - **弱定义位置**:`stm32xx_hal_tim.c`(例如`stm32h7xx_hal_tim.c`) - **用户实现位置**:用户代码中(如`main.c`或自定义的定时器处理文件) #### 2. 函数作用 当定时器的计数器溢出(或达到预设周期值)时,触发更新事件中断,此时`HAL_TIM_PeriodElapsedCallback` 会被调用,用于执行用户自定义的周期性任务,例如:LED闪烁、传感器数据采集等[^3]。 #### 3. 函数原型 ```c __weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { // 默认空实现(需用户覆盖) } ``` #### 4. 使用步骤 1. **定时器初始化** 配置定时器参数(预分频系数、计数周期等),例如: ```c TIM_HandleTypeDef htim3; htim3.Instance = TIM3; htim3.Init.Prescaler = 8399; // 84MHz/(8400) = 10kHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 9999; // 10kHz/10000 = 1Hz HAL_TIM_Base_Init(&htim3); ``` 2. **启动定时器中断** 使用 `HAL_TIM_Base_Start_IT` 启动定时器并开启中断[^3]: ```c HAL_TIM_Base_Start_IT(&htim3); ``` 3. **实现回调函数** 在用户代码中重新定义函数: ```c void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) // 判断触发源 { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 示例:翻转LED } } ``` #### 5. 注意事项 - 若使用多个定时器,需在回调函数中通过 `htim->Instance` 判断具体触发源[^3]。 - 必须调用 `HAL_TIM_Base_Start_IT`(而非 `HAL_TIM_Base_Start`)以启用中断功能。 - 确保中断服务函数 `TIMx_IRQHandler` 已正确关联到定时器(通常由CubeMX自动生成)。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值