从类加载到双亲委派:深入解析类加载机制与 ClassLoader,2024年最新BAT大厂物联网嵌入式开发面试真题锦集干货整理

16 阅读12分钟

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

推荐一个更好的可视化工具,查看字节码的 IDEA 插件:jclasslib

可以很清晰的看到,常量池、接口、字段、方法、属性里面的字节码信息,比如:在对比循环体外实例化对象和循环体内实例化对象差异化时,可以通过该工具来查看两者的区别

在这里插入图片描述

class 文件到底是什么样的呢?

当一个 class 文件躺在硬盘上,该内容被 load 进内存以后,会创建两块内容

  1. 比如:String,将 String,class 二进制内容扔到了内存中
  2. 该内容于是乎生成了一个 class 对象(指令)该指令指向了生成好的二进制内容

在这里插入图片描述

class 存放在内存分区中,内存分区就是存常量、存 class 各种各样的信息,实际上它这块内容逻辑上叫 Method Area 方法区

JDK 8 之前该方法区实现落地在 PermGen 永久代中
JDK 8 之后该方法区实现落地在 Metaspace 元空间中

永久代、元空间两者之间的区别?

  1. 永久代:逻辑上属于堆,物理上不属于堆,存在于虚拟机中
  2. 元空间:逻辑上、物理上都属于堆,但其不存在于虚拟机中,它使用的是物理内存

Class 加载、链接、初始化

在这里插入图片描述

Loading 加载过程:将 class 文件加载进内存中,也就是对 class 文件中的一个个二进制字节码读取后进行装载

Linking 链接过程分为以下三部分:

  1. Verification:校验装载进来的 class 文件是不是符合 class 文件的标准、规范,假设 > 读取进来的 class 文件开头并非以 CA FE BA BE,那么在这个步骤就会被拒绝了
  2. Preparation:将 class 文件中静态成员变量赋予默认值,并非赋予初始值,假设 > static int i = 1,在这个步骤并不会把变量 i 赋值为 1,而是先赋值为 0
  3. Resolution:将类、方法、属性等符号引用解析为直接引用,要给它转换为直接内存地址,直接可以访问到的

Initializing 初始化过程:调用类初始化代码,静态变量在此时会赋予初始化

当该 class 对应所有对象都没有引用指向以后,当发生 GC 时,这些对象资源都会被回收掉

加载、类加载器

类加载器:class 文件从虚拟机加载到内存里都是通过 ClassLoader 类加载器加载进内存的,ClassLoader 是顶级父类,它是一个 abstract 类,它一定会有对应的子类实现,若想查看某个 class 是被哪个加载器加载进内存的,可以调用 类名.class.getClassLoader() 方法进行查看,示例代码如下:

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class ClassLoaderLevel {
    public static void main(String[] args) {
        System.out.println("String:"+ String.class.getClassLoader());
        System.out.println("HKSCS:"+ HKSCS.class.getClassLoader());
        System.out.println("DNSNameService:"+ DNSNameService.class.getClassLoader());
        System.out.println("ClassLoaderLevel:"+ ClassLoaderLevel.class.getClassLoader());
        System.out.println("DNSNameService#parent#classLoader:"+ DNSNameService.class.getClassLoader().getClass().getClassLoader());
        System.out.println("ClassLoaderLevel#parent#classLoader:"+ ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());
    }
}

执行结果及描述如下:

// 输出结果为 null,因为其是顶级加载器 Bootstrap 所加载的
String:null
// 输出结果为 null,因为其也是顶级加载器 Bootstrap 所加载的
HKSCS:null
// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2
// 输出结果为 null,父加载器并不是类的加载器的加载器
DNSNameService#parent#classLoader:null
ClassLoaderLevel#parent#classLoader:null

通过示例代码演示过后,下面来介绍不同层次的类加载器,它们各自所负责的事情是什么

  1. Bootstrap:启动类加载器,又称之为引导类加载器,它用于加载 lib 里 JDK 最核心的内容,比如 > rt.jar、charset.jar 等包下的核心类,当在什么时候调用 getClassLoader 方法拿到的加载器结果是 null 值时,那么就代表是从最顶层的类加载器中进行加载的
  2. Extension:扩展类加载器,加载扩展包各种各样的文件,扩展包一般放在 jdk 安装目录下的 jre/lib/ext/*.jar 包
  3. App:应用加载器,平时经常会用到的类加载器,用它来指定加载 class path 指定的内容
  4. Custom:自定义类加载器

Custom ClassLoader 父类加载器 > application 父类加载器 > Extension 父类加载器 > Bootstrap
它们只是语义上的继承

在这里插入图片描述

双亲委派

如上图,描述类加载其实在内部是遵循双亲委派机制去进行加载的,那么下面来仔细描述当一个 class 文件要被 load 进内存,是怎样的一个加载过程?

  1. 任何一个 class,当你有自定义 ClassLoader 类加载器时,这时候就先尝试去自定义类加载器里面找,它内部维护着缓存,说你有没有已经帮我加载进来了,如果已经加载进来一遍就不需要加载第二遍,它如果没有在自己的自定义缓存找到的话,它并不是直接加载这块内存,它会先去它的父加载器 App 应用加载器中,问父亲你有没有把我这个类加载进来呢
  2. App 应用加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Extension 扩展类加载器
  3. Extension 扩展类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Bootstrap 启动类加载器
  4. Bootstrap 启动类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就往回委托给它的子加载器 Bootstrap Extension 扩展类加载器
  5. Extension 扩展类加载器,说我只负责加载扩展 jar 包里面的类,其他的我一概不知道也找不到,然后一直向下往回委托到 App 应用加载器、Custom ClassLoader 自定义类加载器中去找

整个加载过程,经过了一圈又一圈,才会真正把类加载进来,当我们能够把该类加载进来时叫做成功,若加载不进来,抛出异常 ClassNotFound 类找不到,这整个过程就叫做双亲委派

在这里插入图片描述

为什么要搞双亲委派机制?

主要是为了安全,若任何一个 Class 文件都可以把它加载进内存的话,那我就可以将 java.lang.String 交由给 ClassLoader,将密码存储成 String 类型对象,把 String load 进内存后,打包给客户,然后就可以偷偷摸摸把密码随便传递,这就造成了密码隐私泄露,极其不安全

当出现了双亲委派机制后,就不会这样了,自定义类加载器加载一次,java.lang.String 就产生了警惕性,它会先去上面查有没有加载过,若上面有加载过就不会返回给你,不给你进行重新加载

Launcher 核心类

Launcher 类是 ClassLoader 中的一个包装启动类,Bootstrap、Extension、App 类加载器它们所加载的路径都来自于 Launcher 核心类的源码

Bootstrap ClassLoader 加载路径:System.getProperty(“sun.boot.class.path”)
Extension ClassLoader 加载路径:System.getProperty(“java.ext.dirs”)
App ClassLoader 加载路径:System.getProperty(“java.class.path”)

如上面,示例代码执行的结果来看,可以得知

// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2

Launcher 类来自于 sun,misc 包,ExtClassLoader、AppClassLoader 来自于 Launcher 源码,它默认显示为类名字后面 + 哈希 code 码,$ 符号 > 代表的意思就是 ExtClassLoader、AppClassLoader 都属于 Launcher 类的内部类!

下面通过一段小程序来看看这三个类加载器里到底加载了哪些文件,先通过指定的路径拿到属性值,然后再将指定符号替换为换行符

Windows 通过 ; 符号替换,Mac 通过 : 符号替换

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class ClassLoaderScope {
    public static void main(String[] args) {
        String bootstrapProperty = System.getProperty("sun.boot.class.path");
        System.out.println("Bootstrap ClassLoader:");
        System.out.println(bootstrapProperty.replaceAll(":", System.lineSeparator()));
		System.out.println();
		
        String extProperty = System.getProperty("java.ext.dirs");
        System.out.println("Ext ClassLoader:");
        System.out.println(extProperty.replaceAll(":", System.lineSeparator()));
		System.out.println();
		
        String appProperty = System.getProperty("java.class.path");
        System.out.println("App ClassLoader:");
        System.out.println(appProperty.replaceAll(":", System.lineSeparator()));
    }
}

执行结果如下:

Bootstrap ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/classes

Ext ClassLoader:
/Users/vnjohn/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java

App ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/deploy.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/dnsns.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/jaccess.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/localedata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/nashorn.jar
// ..... 省略其他

ClassLoader 相关源码

实现自定义类加载器之前,先阅读一下 ClassLoader 相关的源码部分

// 当前类加载器的父加载器
private final ClassLoader parent;

/\*\*
 \* name:当前要加载的全限定类名
 \* resolve:是否将符号引用转换为可以直接访问的地址
 \*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
	// getClassLoadingLock(name):为加载类时都获取一把锁
    synchronized (getClassLoadingLock(name)) {
        // 首先,先检查该类是否被加载过了,若加载过了直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
            	// 若父加载器不为空
                if (parent != null) {
                	// 父类先进行加载
                    c = parent.loadClass(name, false);
                } else {
                	// 父加载器为空时,说明当前加载器是启动类加载器:Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 若类找不到就抛出 ClassNotFoundException 异常
            }
            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();
            }
        }
        // 转换为可以执行访问的类
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

loadClass 方法执行过程说明了,当你要加载一个类时只需要调用 ClassLoader#loadClass 方法就能够把该类加载进内存,加载到内存以后它会返回一个 Class 类的对象

经过上面的源码分析,若在自己缓存中没有找到,父加载器中也没有加载成功,最后只能回来自己再去调用 ClassLoader#findClass 方法去加载,它由 protected 修饰受保护的,只能在子类里面去进行访问,如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

该方法实现只有一句话,只能在它的子类里面去进行访问,很简单,我们只需要实现这个方法,就可以自定义类加载器加载我们所需要的类,这个就是钩子函数 > 模版方法设计模式

ClassLoader 相关问题

1、什么时候需要自定义加载器去实现?

Tomcat 在加载自定义的那部分类时(WEB-INF/classes 目录、WEB-INF/lib 目录中的 JAR文件),肯定是需要自定义类加载器(WebAppClassLoader)去加载这些 class 文件的,热部署的实现也是基于此加载器去实现的

2、如何指定类加载器的 parent

通过 super(parent) 指定

如下是示例代码:

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class ClassLoaderParent extends ClassLoader{
    private static ClassLoaderParent PARENT = new ClassLoaderParent();
    
    private static class MyClassLoader extends ClassLoader {
        public MyClassLoader() {
            super(PARENT);
        }
    }
}

3、如何打破双亲委派机制?

1、JDK 1.2 之前,自定义类加载器都必须重写 ClassLoader#loadClass 方法
2、ThreadContextClassLoader 提供了一种机制来绕过双亲委派机制,以实现在特定的线程上下文中加载类,通过 Thread#setContextClassLoader(ClassLoader cl) 方法可以设置线程上下文类加载器
3、在 OSGi 模块化开发框架中,各自应用程序存在自己的模块化机制和类加载器,可以加载同一个类库的不同版本,并且可以加载同名的类。在这种情况下,打破双亲委派机制的主要方式是使用双亲委派模型的变种,即双亲委派模型的扩展

自定义简单 ClassLoader

1、定义一个测试类:HelloWorld 后,通过 javac 编译成 class 文件

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class HelloWorld {
    public void sayHello() {
        System.out.println("Hello Vnjohn");
    }
}

2、自定义类加载器:MyLoader,实现 URLClassLoader 类,重写 findClass 方法,在 findClass 方法块中会用到辅助方法 defineClass > 将字节数组转换为指定类名的 class 类对象

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class MyClassLoader extends URLClassLoader {
    private static final String FILE_PATH = "/Users/vnjohn/Desktop/";

    public MyClassLoader() {
        super(new URL[0]);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // File 创建一个位于 /Users/vnjohn/Desktop 目录,传入的名字是 org.vnjohn.HelloWorld
        // 然后将中间的 . 替换成 / 最后就可以通过全路径找到 class 文件所在位置
        File f = new File(FILE_PATH, name.replace(".", "/").concat(".class"));
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = fis.read()) != 0) {
                baos.write(b);
            }
            // 转换为二进制字节数组
            byte[] bytes = baos.toByteArray();
            baos.close();
            // 通过 defineClass 将字节数组转换为指定类名 name 的 class 类对象
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert fis != null;
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new MyClassLoader();
        Class<?> clazz = l.loadClass("org.vnjohn.HelloWorld");
        Class<?> clazz1 = l.loadClass("org.vnjohn.HelloWorld");
        // 返回 true,class 对象只会加载一次,第二次不会再加载
        System.out.println(clazz == clazz1);
        // 通过反射获取实例对象再访问其方法
        HelloWorld h = (HelloWorld) clazz.newInstance();
        h.sayHello();
        // AppClassLoader 加载器
        System.out.println(l.getClass().getClassLoader());
        // AppClassLoader 加载器
        System.out.println(l.getParent());
        // ClassLoader 默认的加载器就是 AppClassLoader
        System.out.println(getSystemClassLoader());
    }
}

3、执行 main 主方法,结果如下:

true
Hello Vnjohn
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

自定义加密 ClassLoader

自定义类加载器时还可以自己加密 class 文件,防止反编译、篡改,可以进行手动解密、加密,在这里采用简单一点的办法 > 异或处理;异或一次就是加密、再异或一次就是解密,先把 Class 文件读出来进行异或加密,然后再行写回去;还是以测试类:HelloWorld 为例

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class MyClassLoaderWithEncription extends URLClassLoader {
    public MyClassLoaderWithEncription() {
        super(new URL[0]);
    }

    private static final int SEED = 0B10110110;
    private static final String FILE_PATH = "/Users/vnjohn/Desktop/";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // File 创建一个位于 /Users/vnjohn/Desktop 目录,传入的名字是 org.vnjohn.HelloWorld
        // 然后将中间的 . 替换成 / 最后就可以通过全路径找到 class 文件所在位置
        File f = new File(FILE_PATH, name.replace(".", "/").concat(".class"));
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = fis.read()) != 0) {
                baos.write(b ^ SEED);
            }
            // 转换为二进制字节数组
            byte[] bytes = baos.toByteArray();
            baos.close();
            // 通过 defineClass 将字节数组转换为指定类名 name 的 class 类对象
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert fis != null;
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    // 读文件操作
    private static void encFile(String name) throws Exception {
        File f = new File(FILE_PATH, name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        // 写入到 /Users/vnjohn/Desktop 目录下的 .class 文件
        FileOutputStream fos = new FileOutputStream(new File(FILE_PATH, name.replaceAll("\\.", "/").concat(".class")));
        int b;
        while((b = fis.read()) != -1) {
            // 对每一个字节进行异或加密
            fos.write(b ^ SEED);
        }
        fis.close();
        fos.close();
    }

    public static void main(String[] args) throws Exception {
        // 先将文件读出来进行异或加密
        encFile("HelloWorld");
        ClassLoader l = new MyClassLoaderWithEncription();
        Class<?> clazz = l.loadClass("org.vnjohn.HelloWorld");
        Class<?> clazz1 = l.loadClass("org.vnjohn.HelloWorld");
        // 返回 true,class 对象只会加载一次,第二次不会再加载
        System.out.println(clazz == clazz1);
        // 通过反射获取实例对象再访问其方法
        HelloWorld h = (HelloWorld) clazz.newInstance();
        h.sayHello();
        // AppClassLoader 加载器
        System.out.println(l.getClass().getClassLoader());
        // AppClassLoader 加载器
        System.out.println(l.getParent());
        // ClassLoader 默认的加载器就是 AppClassLoader
        System.out.println(getSystemClassLoader());
    }
}

执行结果跟 自定义简单 ClassLoader 执行的结果是一样的!!

打破双亲委派机制伪代码

只是重写 ClassLoader#findClass 方法是无法打破双亲委派机制的,若要打破它只能重写 ClassLoader#loadClass 方法,如下:

/\*\*
 \* @author vnjohn
 \* @since 2023/6/24
 \*/
public class MyClassLoaderBreak extends URLClassLoader {
    private static final String FILE_PATH = "/Users/vnjohn/Desktop/";

    public MyClassLoaderBreak() {
        super(new URL[0]);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        File f = new File(FILE_PATH + name.replaceAll("\\.", "/").concat(".class"));
        if(!f.exists()) {
            return super.loadClass(name);
        }
        try {
            InputStream is = new FileInputStream(f);
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.loadClass(name);
    }

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new MyClassLoaderBreak();
        Class<?> clazz = classLoader.loadClass("HelloWorld");
        // 若在这中间 HelloWorld 改变了代码,下面的类加载器是可以加载到最新代码的.
        classLoader = new MyClassLoaderBreak();
        Class<?> clazzNew = classLoader.loadClass("HelloWorld");
        // false
        System.out.println(clazz == clazzNew);
    }
}

注意:看如下这段代码,改变了这段代码的逻辑:

if(!f.exists()) {
    return super.loadClass(name);
}

首先调用 loadClass 方法去找你要加载的 class 文件,若没找到就会让父加载器去 load,若找到了就自己 load;之前在这里会调用 findLoadedClass 方法去判断在缓存中是否加载过了,若要加载同一个 class 是覆盖不了的,但是这里把整体 ClassLoader 双亲委派机制干掉就行了

Tomcat 热部署就是这么干的,自定义类加载器重新实例化,然后再去加载所需要调用的类,所以说热部署会相当来说比较慢!!

类懒加载顺序

懒加载,规范来说应该是lazy initializing,JVM 规范并没有规定何时去加载,JVM 虚拟机内部实现都是用的懒加载,当什么时候需要用到这个类时才去进行加载,并不是一个 jar 包文件里面有几千个多类,但我只用到了一个类,还要去将它全部加载进来

JVM 严格规定几种情况下会必须初始化,如下:

  1. new、getstatic、putstatic、invokestatic 指令,只单独访问 final 变量不会初始化
  2. java.lang.reflect 对类进行反射调用时
  3. 初始化子类时,其父类会先进行初始化
  4. 虚拟机启动时,被执行的主类 main 方法必须初始化,比如:WebApplication
  5. 动态语言支持:java.lang.invoke.MethodHandle 解析的结果为 REF_getstatic、REF_putstatic、REF_invokestatic 方法句柄时,该类必须初始化

收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。 img img

如果你需要这些资料,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!