精华内容
下载资源
问答
  • 2021-10-16 19:14:38

            在前面 Java虚拟机:对象创建过程与类加载机制、双亲委派模型 文章中,我们介绍了 JVM 的类加载机制以及双亲委派模型,双亲委派模型的类加载过程主要分为以下几个步骤:

    • (1)初始化 ClassLoader 时需要指定自己的 parent 是谁
    • (2)先检查类是否已经被加载过,如果类已经被加载了,直接返回
    • (3)若没有加载则调用父加载器 parent 的 loadClass() 方法进行加载
    • (4)若父加载器为空则默认使用启动类加载器 bootstrap ClassLoader 进行加载
    • (5)如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

            前面文章也提到,如果想要破坏这种机制,那么就自定义一个类加载器(继承自 ClassLoader),并重写其中的 loadClass() 方法,使其不进行双亲委派即可。最经典例子就是 Tomcat 容器的类加载机制了,它实现了自己的类加载器 WebApp ClassLoader,并且打破了双亲委派模型,在每个应用在部署后,都会创建一个唯一的类加载器。

    1、Tomcat 的类加载器结构图:

    (1)Common ClassLoader:加载 common.loader 属性下的 jar,一般是 CATALINA_HOME/lib 目录下,主要是 tomcat 使用以及应用通用的一些类

    (2)Catalina ClassLoader:加载 server.loader 属性下的 jar,默认未配置路径,返回其父加载器即 Common ClassLoader,主要是加载服务器内部可⻅类,这些类应⽤程序不能访问;

    (3)Shared Classloader:加载 share.loader 属性下的jar,默认未配置路径,返回其父加载器即 Common ClassLoader,主要是加载应⽤程序共享类,这些类对 Tomcat 自己不可见;

    只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 项后,才会真正建立 Catalina ClassLoader 和 Shared ClassLoader 的实例,否则在用到这两个类加载器的地方都会用 Common ClassLoader 的实例代替,而默认的配置文件中是没有设置这两个 loader 项的

    (4)WebApp ClassLoader:Tomcat 可以存在多个 WebApp ClassLoader 实例,每个应⽤程序都会有⼀个独⼀⽆⼆的 WebApp ClassLoader,⽤来加载本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。

    2、Tomcat 的类加载流程说明:

    当 Tomcat 使用 WebAppClassLoader 进行类加载时,具体过程如下:

    (1)先在本地 cache 缓存中查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类

    (2)如果 Tomcat 没有加载过这个类,则从系统类加载器的 cache 缓存中查找是否加载过

    (3)如果没有,则使用 ExtClassLoader 类加载器类加载,重点来了,Tomcat 的 WebAppClassLoader 并没有先使用 AppClassLoader 来加载类,而是直接使用了 ExtClassLoader 来加载类。不过 ExtClassLoader 依然遵循双亲委派,它会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。

    比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。

    (4)如果没有加载成功,WebAppClassLoader 就会调用自己的 findClass() 方法由自己来对类进行加载,先在 WEB-INF/classes 中加载,再从 WEB-INF/lib 中加载。

    (5)如果仍然未加载成功,WebAppclassLoader 会委派给 SharedClassLoader,SharedClassLoad 再委派给 CommonClassLoader,CommonClassLoader 委派给 AppClassLoader,直到最终委派给 BootstrapClassLoader,最后再一层一层地在自己目录下对类进行加载。

    (6)都没有加载成功的话,抛出异常。

    3、源码解析:

    (1)WebAppClassLoader 的 loadClass() 方法源码:

    WebappClassLoader 应用类加载器的 loadClass 在他的父类 WebappClassLoaderBase 中

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = null;
            //1. 先在本地cache查找该类是否已经加载过
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
            //2. 从系统类加载器的cache中查找是否加载过
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
            // 3. 尝试用ExtClassLoader类加载器类加载(ExtClassLoader 遵守双亲委派,ExtClassLoader 会使用 Bootstrap ClassLoader 对类进行加载)
            ClassLoader javaseLoader = getJavaseClassLoader();
            try {
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
            // 4. 尝试在本地目录搜索class并加载
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
            // 5. 尝试用系统类加载器(AppClassLoader)来加载
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
         }
        //6. 上述过程都加载失败,抛出异常
        throw new ClassNotFoundException(name);
    }

    (2)WebAppClassLoader 的 findClass() 方法源码:

    public Class<?> findClass(String name) throws ClassNotFoundException {
        // Ask our superclass to locate this class, if possible
        // (throws ClassNotFoundException if it is not found)
        Class<?> clazz = null;
    
        // 先在自己的 Web 应用目录下查找 class
        clazz = findClassInternal(name);
    
        // 找不到 在交由父类来处理
        if ((clazz == null) && hasExternalRepositories) {  
            clazz = super.findClass(name);
        }
        if (clazz == null) {
             throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    4、为什么tomcat要实现自己的类加载机制:

            WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。最主要原因是保证部署在同一个 Web 容器上的不同 Web 应用程序所使用的类库可以实现相互隔离,避免不同项目的相互影响。当然还有其他原因,如:

    (1)保证 Web 容器自身的安全不受部署的 Web 应用程序影响,所以 Tomcat 使用的类库要与部署的应用的类库相互独立

    (2)保证部分基础类不会被同时加载,有些类库 Tomcat 与部署的应用可以共享,比如说 servlet-api

    (3)保证部署在同一个 Web 容器的应用之间的类库可以共享,这听起来好像主要原因相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,会存放在方法区的永久代中,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。比如这时候如果有大量的应用使用 spring 来管理,如果 spring 类库不能共享,那每个应用的 spring 类库都会被加载一次,将会是很大的资源浪费。

    小结:Tomcat 实际上只有 WebAppClassLoader 加载器中打破了双亲委派,其他类加载器还是遵循双亲委派的。 这样做最主要原因是保证同个 Web 容器中的不同 Web 应用程序所使用的类库相互独立,避免相互影响

    参考文章:https://mp.weixin.qq.com/s/OwWUDxHY4Th6decmJeMTgA

    更多相关内容
  • 当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到 JVM。 package com.shendu; public class JvmTest01 { public static final int initData = 666; public int compute() { ...

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

    package com.shendu;
    
    public class JvmTest01 {
        public static final int initData = 666; 
    
        public int compute() { 
            int a = 1;
            int b = 2;
            int c = (a + b) * 10;
            return c;
        }
    
        public static void main(String[] args) {
            new JvmTest01().compute();
        }
    }
    

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

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

    在这里插入图片描述

    以上就是整个从jvm到java中main程序的执行全部过程,main是所有程序的入口。

    整个类加载如图所示

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9whTvuu-1636008372178)(C:\Users\ZUHAO.OUYANG\AppData\Roaming\Typora\typora-user-images\image-20211103172813056.png)]

    加载

    加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。

    类加载阶段:

    (1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。

    (2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。

    (3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。

    验证
    验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:

    文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。

    元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范要求

    字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。

    符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。

    准备
    准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:pirvate static int size = 12;。那么在这个阶段,size的值为0,而不是12。但final修饰的类变量将会赋值成真实的值。

    解析
    解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。

    初始化

    初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。

    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。


    类加载器和双亲委派机制

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

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

    双亲委派机制

    一个类只有被第一次主动使用时,才会被java虚拟机加载
    主动使用的情况(6种)
    1、创建类的实例
    2、访问类的静态变量
    3、调用类的静态方法
    4、反射加载
    5、初始化一个类的子类
    6、java虚拟机启动时被标记为启动类的类

    定义了几个类加载器。

    • AppClassLoader 系统类加载器 负责的目录如下:

      • %JAVA_HOME%/jre/lib
      • -Xbootclasspath 参数指定的目录
      • 系统属性sun.boot.class.path
    • ExtClassLoader 扩展类加载器 负责的目录如下:

      • %JAVA_HOME%/jre/lib/ext
      • 系统属性java.ext.dirs指定的类库
    • Bootstrap classLoader 启动类加载器 负责的目录如下:

      • 环境变量 classpath
      • -cp
      • 系统属性java.class.path

    各个加载器的加载路径的验证

    AppClassLoader 加载路径地址

            String appProperty = System.getProperty("java.class.path");
            for (String s : appProperty.split(";")) {
                System.out.println(s);
            }
    

    打印为:

    C:\Program Files\Java\jdk1.8.0_281\jre\lib\charsets.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\deploy.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\access-bridge-64.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\cldrdata.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\dnsns.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jaccess.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jfxrt.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\localedata.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\nashorn.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunec.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunjce_provider.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunmscapi.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunpkcs11.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\zipfs.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\javaws.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\jce.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfr.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfxswt.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\jsse.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\management-agent.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\plugin.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\resources.jar
    C:\Program Files\Java\jdk1.8.0_281\jre\lib\rt.jar
    D:\tcl_ouyang\demo_coding\jvmclassload\target\classes
    D:\tcl_ouyang\dev_soft\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar
    
    

    虽然打印了这么多的路径,但是其他的jar已经被它的父加载器给加载过了,根据双亲委托机制,其实AppClassLoader只是加载项目中的classpath路径下的类。

    ExtClassLoader 加载路径

    String extDirs = System.getProperty("java.ext.dirs");
    for (String path : extDirs.split(";")) {
    System.out.println(path);
    }
    

    打印

    C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext
    C:\Windows\Sun\Java\lib\ext
    

    Bootstrap classLoader加载路径

     URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
            for (URL url : urLs) {
                System.out.println(url.toExternalForm());
            }
    

    打印

    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/resources.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/rt.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/sunrsasign.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jsse.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jce.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/charsets.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/lib/jfr.jar
    file:/C:/Program%20Files/Java/jdk1.8.0_281/jre/classes
    

    我们在来验证下,各个加载器之间的关系

            System.out.println(JvmTest02.class.getClassLoader());
            System.out.println(JvmTest02.class.getClassLoader().getParent());
            System.out.println(JvmTest02.class.getClassLoader().getParent().getParent());
    

    打印:

    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@1b6d3586
    null
    

    我们可以得出一个结论:AppClassLoader的父加载器是ExtClassLoader,然后我们的引导类加载器是不对外开放的,因为它是C++编写的,它不是一个类


    JVM类加载器是有亲子层级结构的,如下图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IQ6CttY6-1636008372180)(C:\Users\ZUHAO.OUYANG\AppData\Roaming\Typora\typora-user-images\image-20211103173905907.png)]

    这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的 类加载路径中查找并载入目标类。 比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载 器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天 没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的 类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器, 应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。。 双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

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

    • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改 避免类的重复加载:

    • 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

    展开全文
  • Android类加载

    千次阅读 2022-02-10 17:24:35
    Android类加载

    Java中类加载器 

    1.启动类加载器(Bootstrap ClassLoader):这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库,用户无法直接使用。

    2.扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

    3.应用程序类加载器(Application ClassLoader):这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

    4.自定义加载器:用户自己定义的类加载器。

    双亲委托模式

    某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,每一层的加载器都采用这种方式,直到委托给顶层的启动类加载器为止,如果超类无法加载该类,则会将类的加载内容退回给它的下一层。

    • 双亲委托模式:可以避免重复加载,能有效的确保一个类的全局唯一性
    • 如果不使用这种委托模式,那我们就可以随时使用自定义的类来动态替代一些核心的类,存在非常大的安全隐患,比如定义 java.lang.String 替代系统的String等操作。

    Android中类加载器

    Android中包含以下几种ClassLoader

    • BootClassLoader:用来加载Framework层的字节码文件
    • URLClassLoader:加载.jar文件和文件夹中的class,javaWeb等使用,谷歌不用
    • BaseDexClassLoader:PathClassaLoader、DexClassLoader父类
    • PathClassaLoader:加载已经安装到系统中的apk中的class文件
    • DexClassLoader:加载指定目录中的字节码文件(包括aar,apk,jar)

    BaseDexClassLoader为核心类,androidStudio中是看不到BaseDexClassLoader源码的,提供下源码查看地址:BaseDexClassLoader.java

    BaseDexClassLoader加载过程

    1.构造参数

    DexClassLoader是BaseDexClassLoader子类,下面分析参数。

    /**
     * 构造方法
     */
    public class BaseDexClassLoader extends ClassLoader {
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                     String librarySearchPath, ClassLoader parent) {
        }
    }
    
    /**
     * 构造方法
     */
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }

    dexPath:apk/dex/jar文件路径

    optimizedDirectory:是odex将dexPath路径中dex优化后的输出路径,这个路径必须是手机内部路劲。此参数已弃用,自API级别26起不再生效。

    librarySearchPath:需要加载的C/C++库路径

    parent:父加载器(这个比较重要与Android加载class的机制有关)

    2.加载过程

    BaseDexClassLoader构造方法中传入的参数最终会传给DexPathList,BaseDexClassLoader.findClass最终调用DexPathList.findClass。

    /**
     * 构造方法
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent, boolean isTrusted) {
      super(parent);
      //DexPathList
      this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
    
      if (reporter != null) {
          reportClassLoaderChain();
      }
    }
    
    /**
     * BaseDexClassLoader.findClass
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
      List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
      //DexPathList.findClass
      Class c = pathList.findClass(name, suppressedExceptions);
      if (c == null) {
         ClassNotFoundException cnfe = new ClassNotFoundException(
                        "Didn't find class \"" + name + "\" on path: " + pathList);
         for (Throwable t : suppressedExceptions) {
             cnfe.addSuppressed(t);
         }
         throw cnfe;
      }
      return c;
    }
    • DexPathList.findClass最终调用element.findClass,Element[] dexElements是由makeDexElements方法进行赋值。
    • Thinker热修复方案就是将补丁dex插入到dexElements最前端,这样classLoader就会先加载补丁中修复了bug的class文件,由于classLoader双亲委托,再加载原先有bug的class文件时,发现已经有一摸一样的修复了bug的class被加载了,就会直接返回不会再去加载旧class文件,从而完成修复bug的目的。
    /**
      * List of dex/resource (class path) elements.
      * Should be called pathElements, but the Facebook app uses reflection
      * to modify 'dexElements' (http://b/7726934).
      */
    private Element[] dexElements;
    
    /**
      * DexPathList.findClass
      */
    public Class<?> findClass(String name, List<Throwable> suppressed) {
      //dexElements数组
      for (Element element : dexElements) {
           Class<?> clazz = element.findClass(name, definingContext, suppressed);
           if (clazz != null) {
               return clazz;
           }
      }
    
      if (dexElementsSuppressedExceptions != null) {
          suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
      }
      return null;
    }
    
    /**
      * dexElements初始化赋值
      */
    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();
              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                 // Raw dex file (not inside a zip/jar).
                 try {
                    dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    if (dex != null) {
                        elements[elementsPos++] = new Element(dex, null);
                    }
                 } catch (IOException suppressed) {
                     System.logE("Unable to load dex file: " + file, suppressed);
                     suppressedExceptions.add(suppressed);
                 }
              } else {
                 try {
                    dex = loadDexFile(file, optimizedDirectory, loader, elements);
                 } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                     suppressedExceptions.add(suppressed);
                 }
                 if (dex == null) {
                    elements[elementsPos++] = new Element(file);
                 } else {
                    elements[elementsPos++] = new Element(dex, file);
                 }
              }
              if (dex != null && isTrusted) {
                  dex.setTrusted();
              }
          } else {
             System.logW("ClassLoader referenced unknown path: " + file);
          }
      }    
      if (elementsPos != elements.length) {
              elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }
    展开全文
  • 类加载器详解

    千次阅读 2021-08-28 00:13:28
    从概念上来讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类,类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器 无论类加载器的类型如何...

    类加载器的分类

    JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader)

    从概念上来讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类,类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

    无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:

    image-20210826073741259

    所以具体为引导类加载器(BootstrapClassLoader)和自定义类加载器(包括ExtensionClassLoader、Application ClassLoader、User Defined ClassLoader)

    Application ClassLoader也叫System ClassLoader

    image-20210826074203975

    可以看出ExtensionClassLoader、APPClassLoader都间接继承了ClassLoader。

    四类加载器之间的关系可以看做是阶级关系,分为上下级,并不是子父类的继承关系。

    Boot strapClassLoader由C/C++实现的,其他三类加载器是由Java实现的

    package com.dongguo.jvm02;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-9:07
     * @description:
     */
    public class ClassLoader1 {
        public static void main(String[] args) {
            //获取系统类加载器
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
            //获取上层 扩展类加载器
            ClassLoader extClassLoader = systemClassLoader.getParent();
            System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1b6d3586
    
            //获取上层 引用类加载器  获取不到引用类加载器
            ClassLoader bootStrapClassLoader = extClassLoader.getParent();
            System.out.println(bootStrapClassLoader);//null
    
            //对于用户自定义类使用的加载器  默认使用系统类加载器加载
            ClassLoader classLoader = ClassLoader1.class.getClassLoader();
            System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
            //String类使用引导类加载器加载,Java核心类库都是使用引导类加载器加载
            ClassLoader StringClassLoader = String.class.getClassLoader();
            System.out.println(StringClassLoader);//null
        }
    }
    

    虚拟机自带的加载器

    启动类加载器(引导类加载器 BootStrap ClassLoader)

    这个类加载使用C/C++语言实现的,嵌套在JVM内部。

    它用来加载Java的核心库(JAVA HOME/jre/lib/rt.jar.resources.jar或sun.boot.class. path路径下的内容) ,用于提供JVM自身需要的类

    并不继承自java.lang.ClassLoader,没有父加载器。

    加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

    出于安全考虑, Bootstrap启动类加载器只加载包名为java, javax.sun等开头的类

    扩展类加载器(Extension ClassLoader)

    Java语言编写,由sun.misc. Launcher$ExtclassLoader实现

    派生于ClassLoader类

    父类加载器为启动类加载器

    从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

    应用类加载器(系统类加载器 AppClassLoader)

    java语言编写,由sun.misc.Launcher$AppClassLoader实现

    派生于classLoader类

    父类加载器为扩展类加载器

    它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

    该类加载是程序中默认的类加载器,一般来说, Java应用的类都是由它来完成加载

    通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

    package com.dongguo.jvm02;
    
    import com.sun.net.ssl.internal.ssl.Provider;
    import sun.misc.Launcher;
    
    import java.net.URL;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-9:27
     * @description:
     */
    public class ClassLoader2 {
        public static void main(String[] args) {
            System.out.println("------启动类加载器------");
            //获取BootStrapClassLoader能够加载的API的路径
            URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
            for (URL url : urLs) {
                System.out.println(url.toExternalForm());
            }
            /*
            ------启动类加载器------
            file:/E:/software/java/jdk/jre/lib/resources.jar
            file:/E:/software/java/jdk/jre/lib/rt.jar
            file:/E:/software/java/jdk/jre/lib/sunrsasign.jar
            file:/E:/software/java/jdk/jre/lib/jsse.jar
            file:/E:/software/java/jdk/jre/lib/jce.jar
            file:/E:/software/java/jdk/jre/lib/charsets.jar
            file:/E:/software/java/jdk/jre/lib/jfr.jar
            file:/E:/software/java/jdk/jre/classes
             */
            //从上面的路径中随意选择一个类,查看使用的类加载器
            ClassLoader classLoader = Provider.class.getClassLoader();
            System.out.println(classLoader);//null    //说明使用的是BootStrapClassLoader加载
        }
    }
    

    image-20210826094240834

    package com.dongguo.jvm02;
    
    import sun.misc.Launcher;
    import sun.security.ec.ECKeyFactory;
    
    import java.net.URL;
    import java.security.interfaces.ECKey;
    import java.util.Properties;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-9:47
     * @description:
     */
    public class ClassLoader3 {
        public static void main(String[] args) {
            System.out.println("------扩展类加载器------");
            //获取SystemClassLoader能够加载的API的路径
            String extDirs = System.getProperty("java.ext.dirs");
            for (String path : extDirs.split(";")) {
                System.out.println(path);
            }
            /*
            ------扩展类加载器------
            E:\software\java\jdk\jre\lib\ext
            C:\WINDOWS\Sun\Java\lib\ext
            */
            //从上面的路径中随意选择一个类,查看使用的类加载器
            ClassLoader classLoader = ECKeyFactory.class.getClassLoader();
            System.out.println(classLoader);//sun.misc.Launcher$ExtClassLoader@1b6d3586 //扩展类加载器
        }
    }
    

    image-20210826095309409

    用户自定义的加载器

    用户自定义类加载器

    在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式

    为什么要自定义类加载器?

    隔离加载类

    修改类加载的方式

    扩展加载源

    防止源码泄漏

    用户自定义类加载器实现步骤:

    1,开发人员可以通过继承抽象类java . lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求

    2,在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass ()方法,而是建议把自定义的类加载逻辑写在findClass ()方法中

    3·在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLCIassLoader类,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

    自定义用户类加载器大致流程

    package com.dongguo.jvm02;
    
    import java.io.FileNotFoundException;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-10:01
     * @description: 自定义用户类加载器大致流程
     */
    public class CustomClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
    
                byte[] result = getClassFromCustomPath(name);
                if (result == null) {
                    throw new FileNotFoundException();
                } else {
                    return defineClass(name, result, 0, result.length);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            throw new ClassNotFoundException();
        }
    
        private byte[] getClassFromCustomPath(String name) {
            //细节略
            //根据路径读取二进制流的方式将指定类读取到内存中形成字节数组
            //如果指定路径的字节码文件进行了加,则需要在此方法中进行解密操作。
            return null;
        }
        public static void main(String[] args) {
            CustomClassLoader customClassLoader = new CustomClassLoader();
            try {
                Class<?> clazz = Class.forName("your class path", true, customClassLoader);
                Object obj = clazz.newInstance();
                System.out.println(obj.getClass().getClassLoader());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    关于ClassLoader

    ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader (不包括启动类加载器)

    image-20210826101617539

    获取ClassLoader的途径

    image-20210826101756258

    package com.dongguo.jvm02;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-10:18
     * @description: 获取ClassLoader
     */
    public class ClassLoader4 {
        public static void main(String[] args) {
            try {
                //第一种方法
                ClassLoader classLoader1 = Class.forName("java.lang.String").getClassLoader();
                System.out.println(classLoader1);
                //第二种
                ClassLoader classLoader2 = Thread.currentThread().getContextClassLoader();
                System.out.println(classLoader2);
                //第三种
                ClassLoader classLoader3 = ClassLoader.getSystemClassLoader().getParent();
                System.out.println(classLoader3);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    运行结果
    null
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@1b6d3586
    

    类加载器的特点

    1.全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
    2.缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。

    3.双亲委派机制

    双亲委派机制

    Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时, Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

    工作原理

    image-20210826111921911

    1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行

    2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达项层的启动类加载器;

    3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

    自己创建一个java.lang.String类

    package java.lang;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-11:22
     * @description: 自定义的String类
     */
    public class String {
        static {
            System.out.println("执行自定义的String类的静态代码快");
        }
    }
    

    调用自定义的String

    package com.dongguo.jvm02;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-11:20
     * @description:
     */
    public class StringTest {
        public static void main(String[] args) {
            //如果使用自定义的String,会输出静态代码块的内容
            java.lang.String str = new java.lang.String();
            System.out.println("Hello JVM");
        }
    }
    运行结果:
    Hello JVM
    

    发现并没有输出自定义String类的静态代码块内容,说明并非使用的自定义的String类

    没有加载自定义的String类,使用的还是核心api中的String类

    尝试运行自定义的String类

    package java.lang;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-11:22
     * @description: 自定义的String类
     */
    public class String {
        static {
            System.out.println("执行自定义的String类的静态代码快");
        }
    
        public static void main(String[] args) {
            System.out.println("启动自定义的String类");
        }
    }
    

    image-20210826113457186

    类加载器加载自定义的类的加载请求时,由于双亲委派机制,加载请求到达引导类加载器,引导类加载器会去加载Java核心库自带的String类,String类并没有main方法,因此报错

    整个过程并没有加载自定义的java.lang.String类

    反向委托

    image-20210826114147678

    当类加载器加载第三方接口提供的jar时,

    比如SPI接口,SPI属于核心api,首先通过双亲委托机制将类加载请求委托到引导类加载器,引用类加载器加载rt.jar 的SPI核心类,SPI接口中使用有第三方的jar包jdbc.jar。第三方的jar需要使用应用类加载器去加载,这个步骤就叫反向委派,通过线程上下文类加载器(通过调用当前线程的getContextClassLoader()方法获得**Thread.currentThread().getContextClassLoader();**)线程上下文类加载器就是应用类加载器

    优势

    避免类的重复加载

    保护程序安全,防止核心AP1被随意篡改

    自定义类: java.lang.String

    自定义类: java.lang.Shkstart

    在自己创建的java.lang包下创建一个类Shkstart,运行

    package java.lang;
    
    /**
     * @author Dongguo
     * @date 2021/8/26 0026-11:55
     * @description:
     */
    public class Shkstart {
        public static void main(String[] args) {
            System.out.println("hello");
        }
    }
    

    image-20210826115701170

    提示禁止使用包名java.lang,通过双亲委派机制引用类加载器加载Shkstart,发现Shkstart是自定义的类,为防止核心AP1被随意篡改,发生报错。

    沙箱安全机制

    自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件’(rt.jar包中java\lang\string.class) ,报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

    其它

    判断两个Class对象是否为同一个类

    在JVM中表示两个Class对象是否为同一个类存在两个必要条件:

    ​ 类的完整类名必须一致,包括包名。

    ​ 加载这个类的Classoader (指ClassLoader实例对象)必须相同。

    换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

    对类加载器的引用

    JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候, JVM需要保证这两个类型的类加载器是相同的。

    类的主动使用和被动使用

    Java程序对类的使用方式分为:主动使用和被动使用。

    主动使用,又分为七种情况:

    ​ 创建类的实例

    ​ 访问某个类或接口的静态变量,或者对该静态变量赋值

    ​ 调用类的静态方法

    ​ 反射(比如: Class. forName (“com. atguigu.Test”))

    ​ 初始化一个类的子类

    Java虚拟机启动时被标明为启动类的类

    JDK 7开始提供的动态语言支持:java.lang.invoke. MethodHandle实例的解析结果REF getstatic, REF putstatic, REF invokestatic句柄对应的类没有初始化,则初始化

    除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

    展开全文
  • 类加载机制

    千次阅读 2021-06-15 16:18:15
    1、概念 2、类加载步骤 3、类加载器 4、双亲委派 5、类的卸载 6、类中成员加载顺序
  • JVM - 类加载

    万次阅读 2022-01-22 19:34:17
    JVM - 类加载
  • 类加载器一、类加载器的作用二、Java虚拟机类加载器结构1. 引导类(启动类)加载器2. 扩展类加载器3. 系统类加载器三、类加载器的加载机制1. 全盘负责2. 双亲委派3. 缓存机制四、自定义类加载器 一、类加载器的作用 &...
  • 深入理解Java类加载器(ClassLoader)

    万次阅读 多人点赞 2017-06-26 09:34:08
    这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器...
  • Java 类加载器详解

    千次阅读 2021-04-08 11:24:45
    解析:把类中的符号引用转换为直接引用初始化(类)使用卸载:结束生命周期类加载器JVM类加载机制类的初始化类加载方式JVM初始化步骤对象初始化方式参考资料对象的初始化对象初始化过程双亲委派模型自定义类加载器 ...
  • 类加载器详解(自己实现类加载器)

    千次阅读 2020-05-09 13:31:22
    目的:看懂4,并且自己实现一个类加载器 1.类加载器是什么东西 2.类加载器的种类 3.类加载器的机制 4.自己实现一个类加载器 在这里引用大佬的链接,这个是讲的很详细的,如果心急,不想细细研究那就直接看我总结的吧 ...
  • 类加载机制 java类从被加载到JVM到卸载出JVM,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。...
  • 1 引导类加载器 引导类加载器(Boostrap ClassLoader),又叫启动类加载器。 由C/C++语言实现,嵌套在JVM内部。 用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的...
  • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。 这里的自定义加载器指的不是开发人员自己定义的类加载器,而是指的所有继承自ClassLoader...
  • JVM类加载器(详解)

    千次阅读 多人点赞 2021-04-10 13:13:45
    JVM类加载器 1.类加载子系统的作用 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。 2.类加载过程 当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会...
  • jvm之java类加载机制和类加载器(ClassLoader)的详解

    万次阅读 多人点赞 2018-08-13 15:05:46
    当程序主动使用某个类时,如果该类还未被加载到内存中,则...如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 一、类加载过程 1.加载 加载指的是将类的class文件...
  • 上篇说到各类加载器再第二次得到...那么这三个类加载器的实例范围,或者首查找的范围是什么? 1. BootStrapClassLoader的实例范围在sun.boot.class.path中 2. ExtClassLoader的实例范围在java.ext.dirs中 3. AppClas.
  • 类加载机制(整个过程详解)

    千次阅读 2022-02-14 18:04:02
    类加载机制是在我们的真个java的运行阶段中的其中一个阶段。 二:什么是快乐星球(类加载机制) 我们编写的 Java 文件都是以.java 为后缀的文件,编译器会将我们编写的.java 的文件编译成.class 文件,简单来说类加载...
  • Android热修复技术 --- 类加载机制

    千次阅读 2022-03-27 13:34:30
    热修复前言 类加载机制
  • JVM类加载器详解

    千次阅读 2021-01-11 23:12:03
    在上一篇中,通过下面这幅图大致了JVM整体的内部运行结构图,在JVM的结构中,类加载子系统作为连接外部class文件与真正将class文件加载到运行时数据区,承担着重要的作用 类加载器是什么?有什么作用? 1、负责从...
  • JVM类加载过程

    万次阅读 多人点赞 2019-06-20 15:10:25
    1. JVM类加载过程 1.概述 从类的生命周期而言,一个类包括如下阶段: 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下...
  • 目录前言一、双亲委派1.1 类加载器结构1.2 双亲委派二、使用步骤1.引入库2.读入数据总结 前言   在深入openjdk源码全面理解Java类加载器(上 – JVM源码篇) 我们分析了JVM是如何启动,并且初始化...
  • jvm类加载_类加载器种类

    千次阅读 2021-02-28 16:28:06
    类加载器种类在jvm类加载过程中,有一步叫做加载的流程加载 : 根据类的全限定名获取到其定义的二进制字节流,并将其加载到内存中. 此时需要借助类加载器来帮助完成全限定名 : 包名 + 类名类加载器分为4类 :%JAVA_HOME%...
  • JVM教你怎么类加载

    万次阅读 2020-03-02 20:47:38
    类加载
  • 面试官,不要再问我“Java虚拟机类加载机制”了

    万次阅读 多人点赞 2019-10-27 16:28:39
    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程。其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解。 面试题试水 现在有这样一道判断程序输出结果...
  • JVM-01 类加载过程及源码分析

    千次阅读 2021-08-14 22:02:03
    一、类加载过程分析 我们通过ide写的java代码,毫无疑问是最终需要加载到JVM来运行的。试想JVM作为跨语言的平台,能同时支持多种编程语言(js、groory、scala…等)的字节码文件运行,那么在字节码文件和JVM之间,...
  • Java类加载器及Android类加载器基础

    千次阅读 2017-03-07 11:29:24
    引子Android插件化与热更新技术日渐成熟,当你研究这些技术时会发现类加载器在其中占据重要地位。Java语言天生就有灵活性、动态性,支持运行期间动态组装程序,而这一切的基础就是类加载器。Java中的类加载器Java...
  • 类加载机制概念 Java虚拟机把描述类的class文件加载到内存,对其进行校验、转换解析、初始化等操作,最终得到可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。 加载 将class文件读入到内存中,并将其...
  • 文章目录1、Java虚拟机的类加载机制概述2、Java虚拟机中的类加载器2.1、查看类加载器加载的路径2.1.1、查看启动类加载器2.1.2、查看扩展类加载器3、类加载器之间的关系3.1、每个类加载器都有一个父加载器3.2、父加载...
  • Java单元测试和类加载

    千次阅读 2019-12-07 10:25:04
    1 Lambda表达式:相等于匿名内部,实现代码作为方法的参数传统。 函数式接口 变量=(参数列表)->{ 方法体 }; 注意: ->操作符 分成两部分 左侧:(参数列表) 右侧: 方法体 1 左侧的类型可以省略,...
  • Java 虚拟机设计团队有意将类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到 Java 虚拟机外部来实现,以便让应用程序自己来决定如何去获取所需的类,实现这个动作的代码称之为"类加载...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,941,841
精华内容 776,736
关键字:

类加载