`

ClassLoader学习笔记

    博客分类:
  • java
 
阅读更多

针对这篇文章的学习摘录:原文:http://java.chinaitlab.com/base/804400.html

 

classloader最初为了满足java Applet而开发,将类从远程下载到浏览器中运行,后来在web容器和osgi中更为广泛应用。

 

Classloader的作用就是加载编译后的二进制 class JVM中。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

 

而基本上所有的classloader都是java.lang.ClassLoader的一个实例(bootstrap classloader不是classLoader的子类)。ClassLoader根据一个字符串名称找到相应的字节码生成一个Class实例。

它有几个方法:

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<?> c) 链接指定的 Java 类,可能的意思是装入class相关的类。

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个

引导类加载器(bootstrap class loader:它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。(jre/lib下的几个包)。

  扩展类加载器(extensions class loader:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。(jre/lib/ext下的几个包)

  系统类加载器(system class loader:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。(java应用程序的加载类)

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。首先理解JVM对于java类相同的判断。

jvm启动时,会启动jre/rt.jar里的类加载器:bootstrap classloader,用来加载java核心api;然后启动扩展类加载器ExtClassLoader加载扩展类,最后加载用户程序加载器AppClassLoader

通过以下代码可知bootstrap加载了那些核心库:

URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();

      for (int i = 0; i < urls.length; i++) {

        System.out.println(urls[i].toExternalForm());

      }

我们不需要在系统属性CLASSPATH中指定这些核心库,因为JVM在启动的时候就自动加载它们了。

通过以下代码可知extensions class loader加载了什么类库

System.out.println(System.getProperty("java.ext.dirs"));

        ClassLoader extensionClassloader = ClassLoader.getSystemClassLoader().getParent();

        System.out.println("the parent of extension classloader : " + extensionClassloader.getParent());

结果:C:\Program Files\Java\jdk1.6.0_13\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

 the parent of extension classloader : null

结果说明:extension classloadersystem classloaderparent,而bootstrap classloaderextension classloaderparent,但它不是一个实际的classloader,所以为null

Java 虚拟机是如何判定两个 Java 类是相同的?Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。(每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader() 方法就可以获取到此引用。)

了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

假定一个类被两个classloader来加载,采用代理模式就可以保证核心库中的类是唯一的,都是经过同一个classloader加载的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到。

真正完成类的加载工作是通过调用 defineClass 来实现的;而启动类的加载过程是通过调用 loadClass 来实现的。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。一个类的定义加载器是它引用的其它类的初始加载器。

上下文类加载器:

java.lang.Thread 中的方法 getContextClassLoader() setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。如java提供的服务提供者接口SPI,允许第三方为这些接口提供实现。常见的 SPI JDBCJCEJNDIJAXP JBI 等。这些 SPI 的接口由 Java 核心库来提供。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到。

问题在于,SPI 的接口Java 核心库的一部分,是由引导类加载器来加载的;而SPI 实现类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

类加载器与 Web 容器

  对于运行在 Java EE 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

类加载器与 OSGi

  OSGi Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。

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 的类加载器 classLoaderAclassLoaderA 在其模块内部查找类 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() 来设置当前线程的上下文类加载器。

两个相关文章:

http://www.blogjava.net/lhulcn618/archive/2006/05/25/48230.html

http://lyg5623.blog.163.com/blog/static/5327401120088284439854/

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics