`
kapok_fly
  • 浏览: 34922 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

Java类装载体系中的隔离性

阅读更多

原文链接:http://gceclub.sun.com.cn/yuanchuang/week-9/classloader.html

Java类装载体系中的隔离性

作者:盛戈歆

作者简介

盛戈歆,软件工程师,你可以通过shenggexin@topwaver.com与他联系。

正文

Java中类的查找与装载出现的问题总是会时不时出现在Java程序员面前,这并不是什么丢脸的事情,相信没有一个Java程序员没遇到过ClassNotException,因此不要为被人瞅见自己也犯这样的错误而觉得不自然,但是在如果出现了ClassNotFoundException后异常后一脸的茫然,那我想你该了解一下java的类装载的体制了,同时为了进行下面的关于类装载器之间的隔离性的讨论,我们先简单介绍一下类装载的体系结构。

1. Java类装载体系结构

装载类的过程非常简单:查找类所在位置,并将找到的Java类的字节码装入内存,生成对应的Class对象。Java的类装载器专门用来实现这样的过程,JVM并不止有一个类装载器,事实上,如果你愿意的话,你可以让JVM拥有无数个类装载器,当然这除了测试JVM外,我想不出还有其他的用途。你应该已经发现到了这样一个问题,类装载器自身也是一个类,它也需要被装载到内存中来,那么这些类装载器由谁来装载呢,总得有个根吧?没错,确实存在这样的根,它就是神龙见首不见尾的Bootstrap ClassLoader. 为什么说它神龙见首不见尾呢,因为你根本无法在Java代码中抓住哪怕是它的一点点的尾巴,尽管你能时时刻刻体会到它的存在,因为java的运行环境所需要的所有类库,都由它来装载,而它本身是C++写的程序,可以独立运行,可以说是JVM的运行起点,伟大吧。在Bootstrap完成它的任务后,会生成一个AppClassLoader(实际上之前系统还会使用扩展类装载器ExtClassLoader,它用于装载Java运行环境扩展包中的类),这个类装载器才是我们经常使用的,可以调用ClassLoader.getSystemClassLoader() 来获得,我们假定程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类通通会由它来装载,值得尊敬吧。AppClassLoader查找类的区域就是耳熟能详的Classpath,也是初学者必须跨过的门槛,有没有灵光一闪的感觉,我们按照它的类查找范围给它取名为类路径类装载器。还是先前假定的情况,当Java中出现新的类,AppClassLoader首先在类传递给它的父类类装载器,也就是Extion ClassLoader,询问它是否能够装载该类,如果能,那AppClassLoader就不干这活了,同样Extion ClassLoader在装载时,也会先问问它的父类装载器。我们可以看出类装载器实际上是一个树状的结构图,每个类装载器有自己的父亲,类装载器在装载类时,总是先让自己的父类装载器装载(多么尊敬长辈),如果父类装载器无法装载该类时,自己就会动手装载,如果它也装载不了,那么对不起,它会大喊一声:Exception,class not found。有必要提一句,当由直接使用类路径装载器装载类失败抛出的是NoClassDefFoundException异常。如果使用自定义的类装载器loadClass方法或者ClassLoader的findSystemClass方法装载类,如果你不去刻意改变,那么抛出的是ClassNotFoundException。

我们简短总结一下上面的讨论:

1.JVM类装载器的体系结构可以看作是树状结构。

2.父类装载器优先装载。在父类装载器装载失败的情况下再装载,如果都装载失败则抛出ClassNotFoundException或者NoClassDefFoundError异常。

那么我们的类在什么情况下被装载的呢?

2. 类如何被装载

 

在java2中,JVM是如何装载类的呢,可以分为两种类型,一种是隐式的类装载,一种式显式的类装载。

2.1 隐式的类装载

 

隐式的类装载是编码中最常用得方式:

A b = new A();

如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。问题来了,我把代码弄得复杂一点点,但依旧没有任何难度,请思考JVM得装载次序:

package test;
Public class A{
    public void static main(String args[]){
        B b = new B();
    }
}

class B{C c;}

class C{}

揭晓答案,类装载的次序为A->B,而类C根本不会被JVM理会,先不要惊讶,仔细想想,这不正是我们最需要得到的结果。我们仔细了解一下JVM装载顺序。当使用Java A命令运行A类时,JVM会首先要求类路径类装载器(AppClassLoader)装载A类,但是这时只装载A,不会装载A中出现的其他类(B类),接着它会调用A中的main函数,直到运行语句b = new B()时,JVM发现必须装载B类程序才能继续运行,于是类路径类装载器会去装载B类,虽然我们可以看到B中有有C类的声明,但是并不是实际的执行语句,所以并不去装载C类,也就是说JVM按照运行时的有效执行语句,来决定是否需要装载新类,从而装载尽可能少的类,这一点和编译类是不相同的。

2.2 显式的类装载

使用显示的类装载方法很多,我们都装载类test.A为例。

使用Class类的forName方法。它可以指定装载器,也可以使用装载当前类的装载器。例如:

Class.forName("test.A");
它的效果和
Class.forName("test.A",true,this.getClass().getClassLoader());
是一样的。

使用类路径类装载器装载.

ClassLoader.getSystemClassLoader().loadClass("test.A");

使用当前进程上下文的使用的类装载器进行装载,这种装载类的方法常常被有着复杂类装载体系结构的系统所使用。

Thread.currentThread().getContextClassLoader().loadClass("test.A")

使用自定义的类装载器装载类

public class MyClassLoader extends URLClassLoader{
public MyClassLoader() {
        super(new URL[0]);
    }
}
MyClassLoader myClassLoader = new MyClassLoader();
myClassLoader.loadClass("test.A");

MyClassLoader继承了URLClassLoader类,这是JDK核心包中的类装载器,在没有指定父类装载器的情况下,类路径类装载器就是它的父类装载器,MyClassLoader并没有增加类的查找范围,因此它和类路径装载器有相同的效果。

我们已经知道Java的类装载器体系结构为树状,多个类装载器可以指定同一个类装载器作为自己的父类,每个子类装载器就是树状结构的一个分支,当然它们又可以个有子类装载器类装载器,类装载器也可以没有父类装载器,这时Bootstrap类装载器将作为它的隐含父类,实际上Bootstrap类装载器是所有类装载器的祖先,也是树状结构的根。这种树状体系结构,以及父类装载器优先的机制,为我们编写自定义的类装载器提供了便利,同时可以让程序按照我们希望的方式进行类的装载。例如某个程序的类装载器体系结构图如下:

图2:某个程序的类装载器的结构

解释一下上面的图,ClassLoaderA为自定义的类装载器,它的父类装载器为类路径装载器,它有两个子类装载器ClassLoaderAA和ClassLaderAB,ClassLoaderB为程序使用的另外一个类装载器,它没有父类装载器,但有一个子类装载器ClassLoaderBB。你可能会说,见鬼,我的程序怎么会使用这么复杂的类装载器结构。为了进行下面的讨论,暂且委屈一下。

3. 奇怪的隔离性

我们不难发现,图2中的类装载器AA和AB, AB和BB,AA和B等等位于不同分支下,他们之间没有父子关系,我不知道如何定义这种关系,姑且称他们位于不同分支下。两个位于不同分支的类装载器具有隔离性,这种隔离性使得在分别使用它们装载同一个类,也会在内存中出现两个Class类的实例。因为被具有隔离性的类装载器装载的类不会共享内存空间,使得使用一个类装载器不可能完成的任务变得可以轻而易举,例如类的静态变量可能同时拥有多个值(虽然好像作用不大),因为就算是被装载类的同一静态变量,它们也将被保存不同的内存空间,又例如程序需要使用某些包,但又不希望被程序另外一些包所使用,很简单,编写自定义的类装载器。类装载器的这种隔离性在许多大型的软件应用和服务程序得到了很好的应用。下面是同一个类静态变量为不同值的例子。

package test;
public class A {
  public static void main( String[] args ) {
    try {
      //定义两个类装载器
      MyClassLoader aa= new MyClassLoader();
      MyClassLoader bb = new MyClassLoader();

      //用类装载器aa装载testb.B类
      Class clazz=aa.loadClass("testb. B");
      Constructor constructor= 
        clazz.getConstructor(new Class[]{Integer.class});
      Object object = 
	    constructor.newInstance(new Object[]{new Integer(1)});
      Method method = 
	    clazz.getDeclaredMethod("printB",new Class[0]);

      //用类装载器bb装载testb.B类
      Class clazz2=bb.loadClass("testb. B");
      Constructor constructor2 = 
        clazz2.getConstructor(new Class[]{Integer.class});
      Object object2 = 
	    constructor2.newInstance(new Object[]{new Integer(2)});
      Method method2 = 
	    clazz2.getDeclaredMethod("printB",new Class[0]);

      //显示test.B中的静态变量的值 
      method.invoke( object,new Object[0]);
      method2.invoke( object2,new Object[0]);
    } catch ( Exception e ) {
      e.printStackTrace();
    }
  }
}

//Class B 必须位于MyClassLoader的查找范围内,
//而不应该在MyClassLoader的父类装载器的查找范围内。
package testb;
public class B {
    static int b ;

    public B(Integer testb) {
        b = testb.intValue();
    }

    public void printB() {
        System.out.print("my static field b is ", b);
    }
}

public class MyClassLoader extends URLClassLoader{
  private static File file = new File("c:\\classes ");
  //该路径存放着class B,但是没有class A

  public MyClassLoader() {
    super(getUrl());
  }

  public static URL[] getUrl() {
    try {
      return new URL[]{file.toURL()};
    } catch ( MalformedURLException e ) {
      return new URL[0];
    }
  }
}

程序的运行结果为:

my static field b is 1
my static field b is 2

程序的结果非常有意思,从编程者的角度,我们甚至可以把不在同一个分支的类装载器看作不同的java虚拟机,因为它们彼此觉察不到对方的存在。程序在使用具有分支的类装载的体系结构时要非常小心,弄清楚每个类装载器的类查找范围,尽量避免父类装载器和子类装载器的类查找范围中有相同类名的类(包括包名和类名),下面这个例子就是用来说明这种情况可能带来的问题。

假设有相同名字却不同版本的接口 A,

版本 1:
package test;
Intefer Same{ public String getVersion(); }
版本 2:
Package test;
Intefer Same{ public String getName(); }

接口A两个版本的实现:

版本1的实现
package test;
public class Same1Impl implements Same {
public String getVersion(){ return "A version 1";}
}
版本2的实现
public class Same 2Impl implements Same {
public String getName(){ return "A version 2";}
}

我们依然使用图2的类装载器结构,首先将版本1的Same和Same的实现类Same1Impl打成包same1.jar,将版本2的Same和Same的实现类Same1Impl打成包same2.jar。现在,做这样的事情,把same1.jar放入类装载器ClassLoaderA的类查找范围中,把same2.jar放入类装器ClassLoaderAB的类查找范围中。当你兴冲冲的运行下面这个看似正确的程序。

实际上这个错误的是由父类载器优先装载的机制造成,当类装载器ClassLoaderAB在装载Same2Impl类时发现必须装载接口test.Same,于是按规定请求父类装载器装载,父类装载器发现了版本1的test.Same接口并兴冲冲的装载,但是却想不到Same2Impl所希望的是版本2 的test.Same,后面的事情可想而知了,异常被抛出。

我们很难责怪Java中暂时并没有提供区分版本的机制,如果使用了比较复杂的类装载器体系结构,在出现了某个包或者类的多个版本时,应特别注意。

掌握和灵活运用Java的类装载器的体系结构,对程序的系统设计,程序的实现,已经程序的调试,都有相当大的帮助。希望以上的内容能够对您有所帮助。

 

Ext1
Class.forName(String clz);
使用调用这个语句的对象所使用的ClassLoader作为ClassLoader(1)。(请查看代码)

Thread.currentThread().getContextClassLoader())
返回当前线程使用的ClassLoader(2)。


如果ClassLoader(1)“等于”ClassLoader(2),那么这里的使用Class.forName(String clz)和ClassLoader(2).loadClass(String clz)就是等价的。

如果ClassLoader(1)“不等于”ClassLoader(2),那么就是不等价的。

在框架代码中,实际情况是ClassLoader(1)往往“不等于”ClassLoader(2):
即调用Class.forName(String clz)的语句所在的类的装载器不一定就是当前线程使用的类装载器。
因为:一个类装载完了在运行了,该类的所属的类装载器就确定了(不变),而在程序运行之中,线程的类装载器都随时可以通过hread.currentThread().setContextClassLoader(loader)进行改变(变)。

 

Class.forName有两个签名Class.forName(String)和Class.forName(String, boolean, ClassLoader),最终都是调用内部native方法forName0,第一个签名实现是return forName0(className, true, ClassLoader.getCallerClassLoader()),第二个稍微复杂一些,但是大概认为是在当指定loader==null的时候使用ClassLoader.getCallerClassLoader().
再看ClassLoader.getCallerClassLoader()怎么实现,很简单通过一个native方法得到caller class然后getClassLoader()
现在结论很简单了,当caller class不是由current thread的context ClassLoader load的时候,两者的结果会不一致

 

 

 

 

分享到:
评论

相关推荐

    [浅析J2EE应用服务器的JAVA类装载器]python回朔异常的模块.docx

    Java类装载器机制是Java语言灵活性的关键组成部分,尤其在J2EE应用服务器中扮演着重要角色。理解这一机制有助于开发者更好地部署和管理应用程序,解决可能出现的部署问题。 Java类装载器是Java虚拟机(JVM)的一...

    java中四个核心思想

    Java中的类装载器采用了一种层次化的体系结构,主要包括以下几种类型的装载器: - **启动类装载器(Bootstrap Class Loader)**:这是系统级的类装载器,用于装载Java的核心类库,如`java.lang.*`等,它是所有其他类...

    Java四大核心技术思想详解.doc

    类装载器的层次结构使得不同装载器加载的类可以形成独立的命名空间,增强了Java的安全性和隔离性。 接下来,垃圾收集与内存管理是Java的一大特点,它自动处理内存的分配和回收,避免了C++等语言中的内存泄漏问题。...

    深入java虚拟机笔记

    - **类装载器体系结构**:类装载器通过将不同来源的类隔离在不同的命名空间中,提高了系统的安全性。 - **Class文件检验器**:JVM在加载类之前会对Class文件进行严格的校验,以确保其符合安全规范。 - **第一趟:...

    java四大核心思想.pdf

    5. **类装载器的体系结构**:Java应用中可以有多个类装载器,形成一个树状结构。每个类装载器有自己的命名空间,当一个类被装载时,其依赖的类会由装载该类的类装载器进行装载,形成了类的隔离。这种机制支持了模块...

    java ClassLoader机制及其在OSGi中的应用

    Java ClassLoader机制是Java虚拟机(JVM)中一个至关重要的组成部分,它的主要任务是将类的.class文件加载到JVM中,使得程序能够运行。ClassLoader不仅负责类的加载,还涉及类的验证、初始化等一系列过程。理解...

    深入了解java虚拟机,总结得很好

    1. **类装载**:JVM启动时,会先装载主类(程序入口点所在的类),之后根据程序运行过程中需要的其他类按需进行装载。 2. **字节码解释/编译**:JVM中的执行引擎会根据字节码指令进行解释或即时编译(JIT)。 3. **...

    ClassLoader in OSGI

    这种隔离性确保了即使两个bundle中的类具有相同的全限定名,也不会发生冲突。 此外,OSGI还支持类的版本化管理,这意味着不同版本的同一个类可以共存于同一个JVM中,而不会引起任何冲突。这种特性对于构建高度动态...

    Java虚拟机详解

    JVM通过一个抽象层隔离了Java程序与底层硬件之间的差异,确保了Java程序的一致性和兼容性。 **JVM的主要任务**: - **字节码验证**:确保字节码是安全的,不会破坏系统的安全性。 - **字节码执行**:通过解释器或者...

    方案设计架构图汇总情况-java技术框架.doc

    首先,Java类加载器架构是Java虚拟机(JVM)的核心组成部分,负责将类文件从磁盘加载到内存中,执行类的初始化,并管理类的生命周期。它分为Bootstrap ClassLoader、Extension ClassLoader和AppClassLoder三个层次,...

    Java平台及应用Java技术的安全问题分析.pdf

    Java的类装载器体系结构允许动态加载和隔离不同来源的代码,提供了一种强大的模块化机制,同时也为潜在的安全漏洞提供了入口。例如,恶意代码可能利用类装载器的特性来绕过安全检查,执行未经授权的操作。 Java技术...

    消费电子中的Java虚拟机应用于数字电视机顶盒的研究与实现

    7. **安全性与移植性**:Java虚拟机的类装载器体系结构有助于确保安全性和网络移动性,因为它允许动态加载和隔离类。通过JNI,Java程序能够在不牺牲跨平台能力的前提下,利用本地方法访问特定平台的资源。 总的来说...

    Spring.3.x企业应用开发实战(完整版).part2

    3.2.2 类装载器ClassLoader 3.2.3 Java反射机制 3.3 资源访问利器 3.3.1 资源抽象接口 3.3.2 资源加载 3.4 BeanFactory和ApplicationContext 3.4.1 BeanFactory介绍 3.4.2 ApplicationContext介绍 3.4.3 父子容器 ...

    Spring3.x企业应用开发实战(完整版) part1

    3.2.2 类装载器ClassLoader 3.2.3 Java反射机制 3.3 资源访问利器 3.3.1 资源抽象接口 3.3.2 资源加载 3.4 BeanFactory和ApplicationContext 3.4.1 BeanFactory介绍 3.4.2 ApplicationContext介绍 3.4.3 父子容器 ...

    oracal概念手册中英文版

    本章讲述了Oracle数据库如何处理事务,包括事务的ACID属性(原子性、一致性、隔离性、持久性)、回滚与提交机制等内容。 #### 第5章 方案对象 方案对象是指属于某个数据库用户的数据库对象,例如表、视图、存储...

    数据库原理期末复习.pdf

    - **事务**: 数据库操作的基本单位,具有原子性、一致性、隔离性和持久性。 - **事务的性质**: ACID特性。 - **故障的种类**: 包括事务故障、系统故障和介质故障等。 - **恢复的实现技术**: 包括日志记录、检查点等...

    dubbo的初级到高级,分布式系统架构视频

    综上所述,Dubbo作为一款成熟的分布式服务框架,不仅拥有丰富的功能和高度的可扩展性,而且在实际项目中也有广泛的应用。对于希望深入了解分布式系统架构和微服务开发的开发者来说,深入学习Dubbo将会是非常有价值的...

Global site tag (gtag.js) - Google Analytics