jvm-类加载器(五续)

jvm-类加载器(五续)

和将GC垃圾收集器一样,前面将了很多理论,真正工作的是“器”,GC收集器,类加载器。
在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。

说白了就是:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成Java字节代码(.class 文件)。类加载器负责读取 java.class文件,并转换成 java.lang.Class类的一个实例,每个这样的实例用来表示一个Java类。通过此实例的 newInstance()方法就可以创建出该类的一个对象

类与类加载器

类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。
这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

两个完全相同的类,路径也相同,包也相同,但是使用了不同的类加载器,最后也不会相同。每个类加载器都有一个独立的空间,他们被不同的加载器加载到了不同的空间,最后他们也不会相等。

java.lang.ClassLoader类加载器

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,

  • getParent() 返回该类加载器的父类加载器。
  • loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
  • findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
  • findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实例。这个方法被声明为 final的。
  • resolveClass(Class

一点野史

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行

双亲委派模型

从Java虚拟机的角度来说,只存在两种不同的类加载器:

  • 一种是启动类加载器(Bootstrap ClassLoader):这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;
  • 另一种就是所有其他的类加载器:这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。

从开发者的角度,类加载器可以细分为:

  • 启动(Bootstrap)类加载器:负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

    注意:虚拟机为了安全性以及功能的完整性,并不是任何存在于启动类加载器路径下的jar都会被加载,它是通过jar的名字来区分需要加载的类的,如rt.jar等,其它的类即使放在启动类加载器的加载目录下,也是不会被加载的。

  • 标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher.ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
  • 应用程序(Application)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器

除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

类加载的内存分布情况

**引导类加载器**(Bootstrap ClassLoader)加载系统类后,JVM内存会呈现如下格局 ![](https://img-blog.csdn.net/20160116204532583)
  1. 引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
  2. 类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL
  3. 对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
    类加载器就是将.class(字节码)文件转换为java.lang.Class实例。

双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

实现自己的类加载时,重写方法loadClass与findClass的区别

如果实现自己的类加载器时,重写了loadClass方法,则加载类的时候直接加载,不会走双亲代理模式。如果重写的时findClass,则首先会走双亲模式,知道查找不到时,才会走到重写的findClass方法,在根据自己的加载器加载。 **重写loadClass方法** 如果要想在JVM的不同类加载器中保留具有相同全限定名的类,那就要通过重写loadClass来实现,此时首先是通过用户自定义的类加载器来判断该类是否可加载,如果可以加载就由自定义的类加载器进行加载。 这种情况下,就有可能有大量相同的类,被不同的自定义类加载器加载到JVM中,并且这种实现方式是不符合双亲委派模型。但是不能够说这种实现方式就一定是错误的,有可能当前的场景就需要这样的方式,如容器插件应用场景就适合。 **重写findClass方法**

public class ClassLoadDemo extends ClassLoader {

    private String rootDir;
    public ClassLoadDemo(String rootDir) {
        this.rootDir = rootDir;
    }


    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

}
使用自己定义的类加载器加载
public static void main(String[] args) throws ClassNotFoundException {
        ClassLoadDemo demo= new  ClassLoadDemo("");
        demo.loadClass("javassist");
    }
查看ClassLoad源码,可以看到findClass这个地方
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) {

                    long t1 = System.nanoTime();
                    //这里是重点哦
                    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;
        }
    }
根据调用分析: demo.loadClass(“com…..Demo”); 在调用loadClass加载类时首先会调用classLoad方法,开始走双亲模式,当c==null时,开始执行findClass(name)方法,源码中的findClass是直接跑异常的,但是因为我们重写了findClass,这个时候就会走我们自己的类加载方法。

http://blog.csdn.net/fenglibing/article/details/17471659

类加载器的代理模式

类加载器的代理模式也可以说是双亲委派模式,类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。 代理模式是为了保证Java核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。 不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间

加载类的过程

类在加载过程中类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个**类的定义加载器**(defining loader),后者称为**初始加载器**(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。 方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常; 方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

关于ClassNotFoundException与NoClassDefFoundError

1.ClassNotFoundException 当应用程序试图使用以下方法通过字符串名加载类时,抛出该异常: * Class 类中的 forName 方法。 * ClassLoader 类中的 findSystemClass 方法。 * ClassLoader 类中的 loadClass 方法。 但是没有找到具有指定名称的类的定义。从 1.4 版本开始,此异常已经更新,以符合通用的异常链机制。在构造时提供并通过 getException() 方法访问的“加载类时引发的可选异常”,现在被称为原因,它可以通过 Throwable.getCause() 方法以及与上面提到的“遗留方法”来访问。 2.NoClassDefFoundError 当 Java 虚拟机或 ClassLoader 实例试图在类的定义中加载(作为通常方法调用的一部分或者作为使用 new 表达式创建的新实例的一部分),但无法找到该类的定义时,抛出此异常。 当前执行的类被编译时,所搜索的类定义存在,但无法再找到该定义。 NoClassDefFoundError: 网上看到的引起NoClassDefFoundError的三种情况: 1. JAR重复引入,版本不一致导至 2. 打程序版本时,没有把关联类打出去(这种情况一般是) java.lang.nosuchmethoderror 3. 还有一种情况是A引用B时,B初始化失败时也会导致以上的错误出现。 加载时从外存储器找不到需要的class就出现ClassNotFoundException 连接时从内存找不到需要的class就出现NoClassDefFoundError

程序类加载器,当前类加载器与线程上下文类加载器

程序类加载器

也成为系统类加载器因为他是通过systemClassLoader获取的
ClassLoader.getSystemClassLoader()
系统类加载器通常不会使用。 所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。

当前类加载器

当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器 根据对象获取类加载器 ClassLoadDemo demo=new ClassLoadDemo(); demo.getClass(); 根据类型获取类加载器加载器 ClassLoadDemo.class.getClasses(); 静态方法获取类加载器 Class.forName(String) Class.getResource(String) 他们三个是一毛一样的哦。加载出来的实例是同一个哦。

《java基础-java反射》里面有讲解

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。每个线程都有一个关联的上下文类加载器 类 java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,所以如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源;Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(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)

个人理解就是:SPI接口是需要启动类加载器类加载的,但是SPI只是接口,真正的实现是在外面的;在加载过程中,如果使用当前类加载器,则因为是SPI接口,因为是SPI接口则会一直往上加载一直丢到启动类去加载,而启动类看到是SPI接口,在自己加载的范围内,就开始尝试加载类,但是SPI的接口的实现是在核心rt.jar外面的(启动类只加载rt.jar等核心jar),启动类无法加载SPI接口的实现,就会无法加载。
个人疑问?:启动类加载器无法加载就应该让给系统类加载器来加载啊,原因一,SPI是核心类库,系统类加载器按照双亲模型规定,需要交给父类加载器,这个时候交给了启动类加载器,启动类加载器发现是SPI接口,启动类加载器可以加载的,但是发现SPI接口的实现是在核心库外面的, 或者第三方jar里面的, 这个时候启动类就会加载失败而不是说无法加载)。只有判断无法加载的时候才会有子类加载,而SPI接口对于启动类加载器来说,他是可以加载的,只不过加载的时候找不到接口的实现类,因为实现类不在核心库里面,而启动类加载器又只能加载核心库的jar。
如果使用线程上线类加载器来加载,他会直接加载,而不会交给父类加载器类加载,这个时候就可以加载了。系统类加载器也可以加载SPI接口哦,只不过在双亲模式下他不能自己尝试加载,而是要交给父类启动类加载器去加载,而启动类加载器正好可以加载SPI接口,但是无法加载SPI接口的实现类,所以就会加载失败。这个时候用线程上线文类加载器调用系统类加载器,直接加载就OK了。

为什么需要线程上下文类加载器? 通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。 有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。 一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。   更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。   

注意,在实际开发中,不要乱用类加载器,一下用当前类加载器,一下子用上下文类加载器,这样容易混乱。
一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。

上下文类加载器说白了就是:当前线程类加载器只能先交给父类加载,父类加载不了的才自己加载;而引导类加载器只在启动的时候加载,当启动后需要加载系统类时,当前线程无法加载,只能交给父类,而最顶层的父类加载器“引导类加载器”只在启动时加载系统类。这样就导致了启动后想加载系统类加载失败的现象。

顺带说下SPI这个东西

SPI:Service Provider Interface : 服务提供接口。 就是定义一个接口,然后不写实现,实现是让外面的人来写实现类。自己只管定义接口,然后使用接口,至于使用那个实现,完全不用关心。 他是怎么实现的呢?那就是用到了ServiceLoader,ServiceLoader是jdk6里面引进的一个特性。 来看一段代码:
package com.test.messageconsumer


public class MessageConsumer {

    public static void main(String[] args) {
       //通过ServiceLoader.load找到所有实现了MessageService.class这个接口的实现类。
        ServiceLoader<MessageService> serviceLoader =  ServiceLoader.load(MessageService.class);
        for(MessageService service : serviceLoader) {
            System.out.println(service.getMessage());
        }
    }

}
具体而言: 1. 定义一组接口, 假设是 MessageService.class; 2. 写出接口的一个或多个实现(MessageServiceImpl1.class, MessageServiceImpl2.class); 3. 在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件com.test.messageconsumer.MessageService, 文件内容如下: com.test.impl.MessageServiceImpl1 com.test.impl.MessageServiceImpl2 4. 使用 ServiceLoader 来加载配置文件中指定的实现。 SPI 的应用之一是可替换的插件机制。比如查看 JDBC 数据库驱动包,mysql-connector-java-5.1.18.jar 就有一个 /META-INF/services/java.sql.Driver 里面内容是 com.mysql.jdbc.Driver 。

ServiceLoader说白了就是根据接口类型和名字,按照规则去读配置文件,从配置文件里面找到相应的实现类。

参考:https://www.cnblogs.com/lovesqcc/p/5229353.html
http://www.myexception.cn/program/1355384.html

破坏双亲委派模型

线程上线文类加载器就是一种破坏双亲模式的一种模型。

类加载器与web容器

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

类加载器与 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 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

  • 如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
  • 如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
  • 如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

参考

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/#artdownload (深入理解Java类加载器)
http://wolfdream.iteye.com/blog/1131558
http://blog.csdn.net/zhoudaxia/article/details/35897057 线程上下文加载器

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81459984