Java的类加载器

JVM类加载器

1、 父类加载器:

1)  获取父类加载器的方法:getParent()

2)  子类加载器和父类加载器的关系:是委派关系而不是继承关系,这个必须要清楚

3)  对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器的类加载器。因为类加载器如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器(或者说启动类加载器)。

2、 加载器的类型:

1)启动类加载器(bootstrap classloader):它用来加载 Java 的核心库,是用原生代码(本       地代码,与平台有关)来实现的,并不继承自java.lang.ClassLoader。这个类加载器负          责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的, 并且是虚拟机识加的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

2)扩展类加载器(extensions classloader):扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 <Java_Runtime_Home >/lib/ext 或者由系统变量java.ext.dir 指定位置中的类库加载到内存中。

3)应用程序类加载器(application classloader):系统类加载器是由 Sun 的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,由于这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序默认的类加载器

4)用户自定义的类装载器 :用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader。ClassLoader中定义的方法为程序为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其它对象一样,用户自定义的类装载器以有Class类的实例都放在内存中的堆区,而装载的类型信息则都放在方法区(元数据信息)。

3、 类加载器的作用:

1)  完成Class文件的装载。类加载器到本地文件系统中加载Class文件到JVM内存空间中(甚至可以通过FTP加载、或者到其他的web站点去加载)。为了完成加载类的这个职责, java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。也就是说Class本身是Java类的抽象(模板),而Jave类又是Jave对象的抽象(模板)。

2)  ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。ClassLoader提供了一系列的方法,比较重要的方法如下表所示:


3)  类加载器层次结构模型:

Java类加载器有两个比较重要的特征:层次组织结构和代理模式。这两个特征也就是我们平时说的类加载器的双亲委派模型。层次组织结构指的是除了顶层为启动类加载器之外,其余的类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。


4、 双亲代理模型

1)代理模式说的是双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。 

2)双亲委派模型的优点:

l  Java类的加载带有了优先级的层次关系,越是被依赖的底层类越是优先被加载。

l  保证了每一个类加载的唯一性。

3)JVM中类唯一性的判断条件:

在Java虚拟机中,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。通俗来说,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。这里所说的相同,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等情况。

5、 自定义类加载器方法

一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现(loadClass的具体实现如下所示)。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。



6、线程上下文类加载器

1)线程上下文存在的理由:主要是为了弥补双亲代理模型的先天缺陷。

前面提到的类加载器的代理模式并不能解决 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 的实现中都会用到。(这里虽然破坏了双亲委托模型——父类调用子类去获取实现类,但是却成功得弥补了双亲委托模型的固有缺陷)。

2)获取和设置线程上下文类加载器

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


Tomcat类加载器

1、 Tomcat5.X的类加载器模型图:

其中用红圈圈起来的部分属于真正的Tomcat类加载器部分。

1)  系统类加载器:

2)  Common类加载器主要负责加载common目录中的所有jar包,该类库可以被Tomcat以及所有的web应用程序使用;

3)  Catalina类加载器主要用来加载server目录中jar包,该类库可以被Tomcat使用,对所有的web应用程序都不可见;

意义:保证了服务器的自身安全不受web应用程序的影响。

4)  Shared类加载器主要用来加载shared目录中的jar包,该类库可以被所有web应用程序所使用,但是对tomcat不可见;

意义:我们在一个服务器上部署多个项目,如果jar包都一样都,比如我们常用的饿StrutsSpringHibernatejar包,我们完全可以将这些jar包放在该目录下,减少了内存的浪费。

5)  WebApp类加载器,主要是加载每个项目的/WEB-INF/*目录下的所有jar文件和class文件;

意义:实现了不同项目的类库隔离,因此我们放在不同项目lib下的jar包并没有发生过类冲突。

JSP 类加载器,每一个 JSP 文件对应一个类加载器,当服务器检测出 JSP 文件发生了改变,就会丢弃目前的 JSP 类加载器,通过建立一个新的类加载器来实现 JSP 文件的 HotSwap 功能。



注意:以上内容都是以Tomcat5.X作为模板进行讲解的,在Tomcat6.X之后,其目录结构都做了相应的调整。将/common、/server、/shared三个目录合并成一个/lib目录。因此上图中shared类加载器、Catalina类加载器默认情况下没有了,统一由Common类加载器完成相应的加载工作。如果我们要恢复成5.x的文件目录格局,则需要在tomcat/conf/catalina.properties配置文件中进行配置,这里不再做详细讲解。

 

1.       OSGI内存格局:

每个Bundle都有自己的类加载器,该类加载器的父加载器是boot类加载器(引导类加载器)。每个Bundle要使用某个包,就需要委派export了这个包的类加载器去完成加载。Bundel加载器都是平级关系。OSGI的类加载器关系不再是双亲委派模型的树状结构了,而是进一步发展成一种运行时才能确定的网装结构。这就打破了传统项目中类和类之间暗流涌动的局面,将这些关系拿到了台面上,甚至让我们人为管理,这也是OSGI最让人头疼的地方之一。





网络类加载器

  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

  类 NetworkClassLoader负责通过网络下载 Java类字节代码并定义出 Java类。它的实现与 FileSystemClassLoader类似。在通过 NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见下载。

  在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web容器的关系。

  类加载器与 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应用共享的目录下面。

  当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

  在介绍完类加载器与 Web容器的关系之后,下面介绍它与 OSGi的关系。

  类加载器与 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的类加载器 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()来设置当前线程的上下文类加载器。

  总结

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


其他资料:

http://www.iteye.com/topic/240013





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值