精华内容
下载资源
问答
  • 类加载器

    万次阅读 2020-04-26 21:18:03
    类加载器 类与类加载器 判断类是否“相等” 任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。 因此,比较两个类是否“相等”,只有在...

    类加载器

    类与类加载器

    判断类是否“相等”

    任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

    因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

    这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

    加载器种类

    系统提供了 3 种类加载器:

    • 启动类加载器(Bootstrap ClassLoader): 负责将存放在 <JAVA_HOME>\lib 目录中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
    • 扩展类加载器(Extension ClassLoader): 负责加载 <JAVA_HOME>\lib\ext 目录中的所有类库,开发者可以直接使用扩展类加载器。
    • 应用程序类加载器(Application ClassLoader): 由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为“系统类加载器”。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
      classload
      当然,如果有必要,还可以加入自己定义的类加载器。

    双亲委派模型

    什么是双亲委派模型

    双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码)

    工作过程

    如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。

    在 java.lang.ClassLoader 中的 loadClass() 方法中实现该过程。

    为什么使用双亲委派模型

    像 java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。

    相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 classpath 下,那么系统将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证。

    展开全文
  • jvm之java类加载机制和类加载器(ClassLoader)的详解

    万次阅读 多人点赞 2018-08-13 15:05:46
    当程序主动使用某个类时,如果该类还未被加载到内存中,则...如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 一、类加载过程 1.加载 加载指的是将类的class文件...

    手把手写代码:三小时急速入门springboot—企业级微博项目实战--->csdn学院

          当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

                                                        

    一、类加载过程

    1.加载    

        加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

        类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

        通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

    • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
    • 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
    • 通过网络加载class文件。
    • 把一个Java源文件动态编译,并执行加载。

        类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

    2.链接

        当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

        1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

        四种验证做进一步说明:

        文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

        元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

        字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

        符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

       2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

       3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

    3.初始化

        初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

    二、类加载时机

    1. 创建类的实例,也就是new一个对象
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射(Class.forName("com.lyj.load"))
    5. 初始化一个类的子类(会首先初始化子类的父类)
    6. JVM启动时标明的启动类,即文件名和类名相同的那个类    

         除此之外,下面几种情形需要特别指出:

         对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

    三、类加载器

        类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

       JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

     1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

    下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

    public class ClassLoaderTest {
    
    	public static void main(String[] args) {
    		
    		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    		for(URL url : urls){
    			System.out.println(url.toExternalForm());
    		}
    	}
    }

    运行结果:

      2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

      3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

    类加载器加载Class大致要经过如下8个步骤:

    1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
    2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
    3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
    4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
    5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
    6. 从文件中载入Class,成功后跳至第8步。
    7. 抛出ClassNotFountException异常。
    8. 返回对应的java.lang.Class对象。

    四、类加载机制:

    1.JVM的类加载机制主要有如下3种。

    • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
    • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
    • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

    2.这里说明一下双亲委派机制:

           双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

          双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

    展开全文
  • 1 线程上下文类加载器 2 何时使用Thread.getContextClassLoader()? 3 类加载器与Web容器 4 类加载器与OSGi 总结 1 线程上下文类加载器  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的...

    目录

    1 线程上下文类加载器

    2 何时使用Thread.getContextClassLoader()?

    3 类加载器与Web容器

    4 类加载器与OSGi

    总结


    1 线程上下文类加载器

      线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
      前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
      线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
      Java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。以下代码摘自sun.misc.Launch的无参构造函数Launch()。

    // Now create the class loader to use to launch the application
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
    "Could not create application class loader" );
    }
    
    
    // Also set the context class loader for the primordial thread.
    Thread.currentThread().setContextClassLoader(loader);

      使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有:通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。大部分java application服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。还有一些采用hot swap特性的框架,也使用了线程上下文类加载器,比如 seasar (full stack framework in japenese)。
      线程上下文从根本解决了一般应用不能违背双亲委派模式的问题。使java类加载体系显得更灵活。随着多核时代的来临,相信多线程开发将会越来越多地进入程序员的实际编码过程中。因此,在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。
      当然,好东西都有利弊。使用线程上下文加载类,也要注意保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器导致类型转换异常(ClassCastException)。
      defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供给开发人员,用来自定义加载class的接口。使用该接口,可以动态的加载class文件。例如在jdk中,URLClassLoader是配合findClass方法来使用defineClass,可以从网络或硬盘上加载class。而使用类加载接口,并加上自己的实现逻辑,还可以定制出更多的高级特性。
      下面是一个简单的hot swap类加载器实现。hot swap即热插拔的意思,这里表示一个类已经被一个加载器加载了以后,在不卸载它的情况下重新再加载它一次。我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。我们从URLClassLoader继承,加载类的过程都代理给系统类加载器URLClassLoader中的相应方法来完成。

    package classloader;
    
    
    import java.net.URL;
    import java.net.URLClassLoader;
    
    
    /**
     * 可以重新载入同名类的类加载器实现
     * 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态
     */
    public class HotSwapClassLoader extends URLClassLoader {
    
    
        public HotSwapClassLoader(URL[] urls) {
            super(urls);
        }
    
    
        public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
    
    
        // 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass()
        // 具体的加载过程代理给父类中的相应方法来完成
        public Class<?> load(String name) throws ClassNotFoundException {
            return load(name, false);
        }
    
    
        public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {
            // 若类已经被加载,则重新再加载一次
            if (null != super.findLoadedClass(name)) {
                return reload(name, resolve);
            }
            // 否则用findClass()首次加载它
            Class<?> clazz = super.findClass(name);
            if (resolve) {
                super.resolveClass(clazz);
            }
            return clazz;
        }
    
    
        public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {
            return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(
                    name, resolve);
        }
    }

      两个重载的load方法参数与ClassLoader类中的两个loadClass()相似。在load的实现中,用findLoadedClass()查找指定的类是否已经被祖先加载器加载了,若已加载则重新再加载一次,从而放弃了双亲委派的方式(这种方式只会加载一次)。若没有加载则用自身的findClass()来首次加载它。
      下面是使用示例:

    package classloader;
    
    
    public class A {
        
        private B b;
    
    
        public void setB(B b) {
            this.b = b;
        }
    
    
        public B getB() {
            return b;
        }
    }
    package classloader;
    
    
    public class B {
        
    }
    package classloader;
    
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    
    public class TestHotSwap {
    
    
        public static void main(String args[]) throws MalformedURLException {
            A a = new A();  // 加载类A
            B b = new B();  // 加载类B
            a.setB(b);  // A引用了B,把b对象拷贝到A.b
            System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());
            System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());
            System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());
    
    
            try {
                URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };
                HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());
                Class clazz = c1.load("classloader.A");  // 用hot swap重新加载类A
                Object aInstance = clazz.newInstance();  // 创建A类对象
                Method method1 = clazz.getMethod("setB", B.class);  // 获取setB(B b)方法
                method1.invoke(aInstance, b);    // 调用setB(b)方法,重新把b对象拷贝到A.b
                Method method2 = clazz.getMethod("getB");  // 获取getB()方法
                Object bInstance = method2.invoke(aInstance);  // 调用getB()方法
                System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());
            } catch (MalformedURLException | ClassNotFoundException | 
                    InstantiationException | IllegalAccessException | 
                    NoSuchMethodException | SecurityException | 
                    IllegalArgumentException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    运行输出:

    A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

      HotSwapClassLoader加载器的作用是重新加载同名的类。为了实现hot swap,一个类在加载过后,若重新再加载一次,则新的Class object的状态会改变,老的状态数据需要通过其他方式拷贝到重新加载过的类生成的全新Class object实例中来。上面A类引用了B类,加载A时也会加载B(如果B已经加载,则直接从缓存中取出)。在重新加载A后,其Class object中的成员b会重置,因此要重新调用setB(b)拷贝一次。你可以注释掉这行代码,再运行会抛出java.lang.NullPointerException,指示A.b为null。
      注意新的A Class object实例所依赖的B类Class object,如果它与老的B Class object实例不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException),表示两种不同的类。因此在重新加载A后,要特别注意给它的B类成员b传入外部值时,它们是否由同一个类加载器加载。为了解决这种问题, HotSwapClassLoader自定义的l/oad方法中,当前类(类A)是由自身classLoader加载的, 而内部依赖的类(类B)还是老对象的classLoader加载的。

    2 何时使用Thread.getContextClassLoader()?

      这是一个很常见的问题,但答案却很难回答。这个问题通常在需要动态加载类和资源的系统编程时会遇到。总的说来动态加载资源时,往往需要从三种类加载器里选择:系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器。在程序中应该使用何种类加载器呢?
      系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。
      因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。
      线程上下文类加载器在Java 2(J2SE)时引入。每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。
      为什么要引入线程的上下文类加载器?将它引入J2SE并不是纯粹的噱头,由于Sun没有提供充分的文档解释说明这一点,这使许多开发者很糊涂。实际上,上下文类加载器为同样在J2SE中引入的类加载代理机制提供了后门。通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。
      有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。
      顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前类加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。
      好了,现在我们明白了问题的关键:这两种选择不可能适应所有情况。一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。
      更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。
      这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:
      (1)JNDI使用线程上下文类加载器。
      (2)Class.getResource()和Class.forName()使用当前类加载器。
      (3)JAXP使用上下文类加载器。
      (4)java.util.ResourceBundle使用调用者的当前类加载器。
      (5)URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。
      (6)Java序列化API缺省使用调用者当前的类加载器。
      这些类加载器非常混乱,没有在J2SE文档中给以清晰明确的说明。
      该如何选择类加载器?
      如若代码是限于某些特定框架,这些框架有着特定加载规则,则不要做任何改动,让框架开发者来保证其工作(比如应用服务器提供商,尽管他们并不能总是做对)。如在Web应用和EJB中,要使用Class.gerResource来加载资源。
      在其他情况下,我们可以自己来选择最合适的类加载器。可以使用策略模式来设计选择机制。其思想是将“总是使用上下文类加载器”或者“总是使用当前类加载器”的决策同具体实现逻辑分离开。往往设计之初是很难预测何种类加载策略是合适的,该设计能够让你可以后来修改类加载策略。
      考虑使用下面的代码,这是作者本人在工作中发现的经验。这儿有一个缺省实现,应该可以适应大部分工作场景:

    package classloader.context;
    
    
    /**
     * 类加载上下文,持有要加载的类
     */
    public class ClassLoadContext {
    
    
        private final Class m_caller;
    
    
        public final Class getCallerClass() {
            return m_caller;
        }
    
    
        ClassLoadContext(final Class caller) {
            m_caller = caller;
        }
    }
    
    package classloader.context;
    
    
    /**
     * 类加载策略接口
     */
    public interface IClassLoadStrategy {
    
    
        ClassLoader getClassLoader(ClassLoadContext ctx);
    }
    /**
     * 缺省的类加载策略,可以适应大部分工作场景
     */
    public class DefaultClassLoadStrategy implements IClassLoadStrategy {
    
    
        /**
         * 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器
         * 和当前线程上下文类加载中选择一个最底层的加载器
         * @param ctx
         * @return 
         */
        @Override
        public ClassLoader getClassLoader(final ClassLoadContext ctx) {
            final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
            final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader result;
    
    
            // If 'callerLoader' and 'contextLoader' are in a parent-child
            // relationship, always choose the child:
            if (isChild(contextLoader, callerLoader)) {
                result = callerLoader;
            } else if (isChild(callerLoader, contextLoader)) {
                result = contextLoader;
            } else {
                // This else branch could be merged into the previous one,
                // but I show it here to emphasize the ambiguous case:
                result = contextLoader;
            }
            final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
            // Precaution for when deployed as a bootstrap or extension class:
            if (isChild(result, systemLoader)) {
                result = systemLoader;
            }
            
            return result;
        }
        
        // 判断anotherLoader是否是oneLoader的child
        private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
            //...
        }
    
    
        // ... more methods 
    }

      决定应该使用何种类加载器的接口是IClassLoaderStrategy,为了帮助IClassLoadStrategy做决定,给它传递了个ClassLoadContext对象作为参数。ClassLoadContext持有要加载的类。
      上面代码的逻辑很简单:如调用类的当前类加载器和上下文类加载器是父子关系,则总是选择子类加载器。对子类加载器可见的资源通常是对父类可见资源的超集,因此如果每个开发者都遵循J2SE的代理规则,这样做大多数情况下是合适的。
      当前类加载器和上下文类加载器是兄弟关系时,决定使用哪一个是比较困难的。理想情况下,Java运行时不应产生这种模糊。但一旦发生,上面代码选择上下文类加载器。这是作者本人的实际经验,绝大多数情况下应该能正常工作。你可以修改这部分代码来适应具体需要。一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。
      最后需要检查一下,以便保证所选类加载器不是系统类加载器的父亲,在开发标准扩展类库时这通常是个好习惯。
      注意作者故意没有检查要加载资源或类的名称。Java XML API成为J2SE核心的历程应该能让我们清楚过滤类名并不是好想法。作者也没有试图检查哪个类加载器加载首先成功,而是检查类加载器的父子关系,这是更好更有保证的方法。
      下面是类加载器的选择器:

    package classloader.context;
    
    
    /**
     * 类加载解析器,获取最合适的类加载器
     */
    public abstract class ClassLoaderResolver {
            
        private static IClassLoadStrategy s_strategy;  // initialized in <clinit>
        private static final int CALL_CONTEXT_OFFSET = 3;  // may need to change if this class is redesigned
        private static final CallerResolver CALLER_RESOLVER;  // set in <clinit>
        
        static {
            try {
                // This can fail if the current SecurityManager does not allow
                // RuntimePermission ("createSecurityManager"):
                CALLER_RESOLVER = new CallerResolver();
            } catch (SecurityException se) {
                throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);
            }
            s_strategy = new DefaultClassLoadStrategy();  //默认使用缺省加载策略
        }
    
    
        /**
         * This method selects the best classloader instance to be used for
         * class/resource loading by whoever calls this method. The decision
         * typically involves choosing between the caller's current, thread context,
         * system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}
         * instance established by the last call to {@link #setStrategy}.
         * 
         * @return classloader to be used by the caller ['null' indicates the
         * primordial loader]
         */
        public static synchronized ClassLoader getClassLoader() {
            final Class caller = getCallerClass(0); // 获取执行当前方法的类
            final ClassLoadContext ctx = new ClassLoadContext(caller);  // 创建类加载上下文
            return s_strategy.getClassLoader(ctx);  // 获取最合适的类加载器
        }
    
    
        public static synchronized IClassLoadStrategy getStrategy() {
            return s_strategy;
        }
    
    
        public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {
            final IClassLoadStrategy old = s_strategy;  // 设置类加载策略
            s_strategy = strategy;
            return old;
        }
    
    
        /**
         * A helper class to get the call context. It subclasses SecurityManager
         * to make getClassContext() accessible. An instance of CallerResolver
         * only needs to be created, not installed as an actual security manager.
         */
        private static final class CallerResolver extends SecurityManager {
            @Override
            protected Class[] getClassContext() {
                return super.getClassContext();  // 获取当执行栈的所有类,native方法
            }
    
    
        }
    
    
        /*
         * Indexes into the current method call context with a given
         * offset.
         */
        private static Class getCallerClass(final int callerOffset) {
            return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET
                    + callerOffset];  // 获取执行栈上某个方法所属的类
        }
    }

      可通过调用ClassLoaderResolver.getClassLoader()方法来获取类加载器对象,并使用其ClassLoader的接口如loadClass()等来加载类和资源。此外还可使用下面的ResourceLoader接口来取代ClassLoader接口:

    package classloader.context;
    
    
    import java.net.URL;
    
    
    public class ResourceLoader {
    
    
        /**
         * 加载一个类
         * 
         * @param name
         * @return 
         * @throws java.lang.ClassNotFoundException 
         * @see java.lang.ClassLoader#loadClass(java.lang.String)
         */
        public static Class<?> loadClass(final String name) throws ClassNotFoundException {
            //获取最合适的类加载器
            final ClassLoader loader = ClassLoaderResolver.getClassLoader();
            //用指定加载器加载类
            return Class.forName(name, false, loader);
        }
    
    
        /**
         * 加载一个资源
         * 
         * @param name
         * @return 
         * @see java.lang.ClassLoader#getResource(java.lang.String)
         */
        public static URL getResource(final String name) {
            //获取最合适的类加载器
            final ClassLoader loader = ClassLoaderResolver.getClassLoader();
            //查找指定的资源
            if (loader != null) {
                return loader.getResource(name);
            } else {
                return ClassLoader.getSystemResource(name);
            }
        }
    
    
        // ... more methods ...
    }

      ClassLoadContext.getCallerClass()返回的类在ClassLoaderResolver或ResourceLoader使用,这样做的目的是让其能找到调用类的类加载器(上下文加载器总是能通过Thread.currentThread().getContextClassLoader()来获得)。注意调用类是静态获得的,因此这个接口不需现有业务方法增加额外的Class参数,而且也适合于静态方法和类初始化代码。具体使用时,可以往这个上下文对象中添加具体部署环境中所需的其他属性。

    3 类加载器与Web容器

      对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
      绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
      (1)每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
      (2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
      (3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

    4 类加载器与OSGi

      OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于OSGi 技术来构建的。
      OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
      假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。
      OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
      (1)如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
      (2)如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
      (3)如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

    总结

      类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException和 NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。

    参考文献:

    https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

    http://www.blogjava.net/lihao336/archive/2009/09/17/295489.html

    http://kenwublog.com/structure-of-java-class-loader

    展开全文
  • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。 这里的自定义加载器指的不是开发人员自己定义的类加载器,而是指的所有继承自ClassLoader...

    1.类加载器分类

    JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
    这里的自定义加载器指的不是开发人员自己定义的类加载器,而是指的所有继承自ClassLoader的类加载器。包括扩展类加载器、应用程序类加载器、用户自定义类加载器(程序员自己写的)三种。
    在这里插入图片描述
    常见的类加载器如下图所示:
    第一个蓝色框表示的是引导类加载器;剩下的所有的类加载器都是自定义的类加载器。BootStrapClassLoader使用C语言实现,自定义类加载器使用Java语言实现。
    注意:图中不是表示的继承关系。扩展类加载器和系统类加载器(应用程序类加载器AppClassLoader)都是继承自ClassLoader,所以将他们划分为自定义加载器。
    在这里插入图片描述
    下面的Java例子,可以帮助理解类加载器之间的关系:

    public class ClassLoaderTest {
        public static void main(String[] args) {
    
            //获取系统类加载器,输出AppClassLoader,说明系统类加载器就是应用程序类加载器
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
            //获取系统类加载器的上层:输出ExtClassLoader,说明系统类加载器的上层是扩展类加载器
            ClassLoader extClassLoader = systemClassLoader.getParent();
            System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
    
            //获取扩展类加载器的上层:输出null,获取不到引导类加载器。虽然获取不到,但是扩展类加载器的上层是引导类加载器
            ClassLoader bootstrapClassLoader = extClassLoader.getParent();
            System.out.println(bootstrapClassLoader);//null
    
            //输出AppClassLoader。说明对于用户自定义类来说:默认使用系统类加载器进行加载
            ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
            System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
            //打印的也是null,和获取扩展类的上层的输出是一样。可以证明String类使用引导类加载器进行加载的。扩展开来:---> Java的核心类库都是使用引导类加载器进行加载的。
            ClassLoader classLoader1 = String.class.getClassLoader();
            System.out.println(classLoader1);//null
    
    
        }
    }
    
    

    1.1 引导类加载器(Bootstrap ClassLoader)

    1.引导类加载器使用C/C++语言实现,在JVM内部
    2.用于加载Java核心类库
    3.不继承ClassLoader
    4.还用于加载扩展类加载器和应用程序类加载器
    5.只加载包名为java,javax,sun开头的类
    在这里插入图片描述

    1.2 扩展类加载器(Extension ClassLoader)

    1.使用java语言编写,JVM自带
    2.继承自ClassLoader
    3.父类加载器为扩展类加载器
    4.从java.ext.dirs指定的路径下加载类库;或者从JDK安装目录的jre/lib/ext目录下加载类库。
    5.如果用户自定义的jar包放在jre/lib/ext下,也会自动由扩展类加载器加载
    在这里插入图片描述

    1.3 应用程序类加载器(AppClassLoader或者称为系统类加载器)

    1.使用jaca语言编写,JVM自带
    2.继承自ClassLoader
    3.父类加载器为启动类加载器
    4.负责加载环境变量classpath或系统属性java.class.path指定的类库
    5.java中自己写的类都是由应用程序类加载器加载的
    6.可以通过ClassLoader.getSystemClassLoader()方法获取该类加载器
    在这里插入图片描述

    理解BootstrapClassLoader、ExtClassLoader、AppClassLoader的例子:

    public class ClassLoaderTest1 {
        public static void main(String[] args) {
            System.out.println("**********启动类加载器**************");
            //获取BootstrapClassLoader能够加载的api的路径
            URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();//获取到的是通过引导类加载的类库的路径(输出参考“引导类能够加载的类库路径”)
            for (URL element : urLs) {
                System.out.println(element.toExternalForm());
            }
            //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
            ClassLoader classLoader = Provider.class.getClassLoader();
            System.out.println(classLoader); //输出为null。说明是引导类加载器加载的
    
            System.out.println("***********扩展类加载器*************");
            String extDirs = System.getProperty("java.ext.dirs");
            for (String path : extDirs.split(";")) {// 输出参考“扩展类能够加载的类库路径”图
                System.out.println(path);
            }
    
            //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
            ClassLoader classLoader1 = CurveDB.class.getClassLoader();
            System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
    
        }
    }
    

    引导类能够加载的类库路径:如下图。主要就是jre/lib/目录下面的jar包。
    在这里插入图片描述
    扩展类能够加载的类库路径:如下图。主要是jre/lib/ext目录以及java.ext.dirs指定的路径下的jar包
    在这里插入图片描述

    1.4 用户自定义类加载器(程序员自己写的)

    除了上面3种JVM提供的类加载器之外,程序员还可以自己定义类加载器。(简单了解)
    在这里插入图片描述
    自定义加载器实现步骤:
    在这里插入图片描述
    自定义一个类加载器简单的例子:

    public class CustomClassLoader extends ClassLoader { //继承ClassLoader
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException { //重载findClass方法
    
            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(name);
        }
    
        private byte[] getClassFromCustomPath(String name){
            //从自定义路径中加载指定类:细节略
            //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。(这里可以进行解密操作,防止class文件被反编译)
            return null;
        }
    
        public static void main(String[] args) {
            CustomClassLoader customClassLoader = new CustomClassLoader();
            try {
                Class<?> clazz = Class.forName("One",true,customClassLoader);
                Object obj = clazz.newInstance();
                System.out.println(obj.getClass().getClassLoader());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

    更多JVM文章请参考我的JVM专栏:https://blog.csdn.net/u011069294/category_10113093.html

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 34,547
精华内容 13,818
关键字:

类加载器