`
blue2048
  • 浏览: 178159 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java 类加载器详解

阅读更多

这是前几天在看类加载器机制时搜到的一篇旧文,网上搜了搜相应的中文资料,感觉很多意思没有翻译出来,这两天我试着自己翻译了一下,供同道参考。英文文章地址:Find a way out of the ClassLoader maze

 

 

走出类加载器迷宫(本人翻译,转载请注明出处

 

系统类加载器当前类加载器上下文类加载器你应该用哪一个?

By Vladimir Roubtsov, JavaWorld.com, 06/06/03

June 6, 2003

Q:我什么时候Thread.getContextClassLoader()?

A:这个问题虽然不常见,却很难正确回答。它一般出现在框架编程中,作为解决类和资源动态加载的一个好方法。总的来说,当动态加载一个资源时,至少有三种类加载器可供选择: 系统类加载器(也被称为应用类加载器)(system classloader),当前类加载器current classloader),和当前线程的上下文类加载器( the current thread context classloader)。上面提到的问题指的是最后一种加载器。哪一个类加载器是正确的?

 

容易排除的一个选择:系统类加载器。这个类加载器处理classpath环境变量所指定的路径下的类和资源,可以通过ClassLoader.getSystemClassLoader()方法以编程式访问。所有的ClassLoader.getSystemXXX()API方法也是通过这个类加载器访问。你应该很少写代码来显式调用,而是以其它的类加载器委托给系统类加载器来代替。否则,当系统类加载器是JVM创建的最后一个类加载器时你的代码将只能工作在简单的命令行应用中。只要你把代码迁移到EJB,web应用,或Java Web Start应用中肯定会出问题。

 

所以,现在我们是两个选择:当前类加载器上下文类加载器。根据定义,当前类加载器加载和定义当前方法所属的那个类。这个类加载器在你使用带单个参数的Class.forName()方法,Class.getResource()方法和相似方法时会在运行时类的链接过程中被隐式调用。它也出现在像X.class语法的字母调用中。(参见"Get a Load of That Name!"获取详细信息)

 

线程上下文类加载器是在J2SE中被引进的。每一个线程分配一个上下文类加载器(除非线程由本地代码创建)。该加载器是通过Thread.setContextClassLoader()方法来设置。如果你在线程构造后不调用这个方法,这个线程将会从它的父线程(译者注:这里的父线程是指执行创建新线程对象语句的线程)中继承上下文类加载器。如果你在整个应用中不做任何设置,所有线程将以系统类加载器作为它们自己的上下文加载器。重要的是明白自从Web和J2EE应用服务器为了像JNDI,线程池,组件热部署等特性而采用复杂的类加载器层次结构后,这(译者注:指整个应用中不做任何设置)是很少见的情况。

 

为什么线程上下文类加载器在第一位?它们被介绍进J2SE时并没有大张旗鼓。从Sun公司缺乏正确的指导和文档或许解释了为什么许多开发人员会对此概念困惑。

 

事实上,上下文类加载器提供了一个后门绕过在J2SE中介绍的类的加载委托机制。通常情况下,一个JVM中的所有类加载器被组织成一个层次结构,使得每一个类加载器(除了启动整个JVM的原始类加载器)都有一个父加载器。当被要求加载一个类时,每一个类加载器都将先委托父加载器来加载,只有父加载器都不能成功加载时当前类加载器才会加载。

 

有时这种加载顺序不能正常工作,通常发生在有些JVM核心代码必须动态加载由应用程序开发人员提供的资源时。以JNDI举例:它的核心内容(从J2SE1.3开始)在rt.jar中的引导类中实现了,但是这些JNDI核心类可能加载由独立厂商实现和部署在应用程序的classpath中的JNDI提供者。这个场景要求一个父类加载器(这个例子中的原始类加载器,即加载rt.jar的加载器)去加载一个在它的子类加载器(系统类加载器)中可见的类。此时通常的J2SE委托机制不能工作,解决办法是让JNDI核心类使用线程上下文加载器,从而有效建立一条与类加载器层次结构相反方向的“通道”达到正确的委托。

 

另外,上段可能提醒你别的事情:用作XML解析的Java API(JAXP)。是的,当JAXP只是J2SE的扩展时,XML解析工厂使用当前类加载器作为启动解析器的实现。当JAXP作为J2SE1.4核心的一部分时,类加载改为使用线程上下文类加载器,JNDI情况完全类似(使很多程序员困惑)。明白我说缺少来自Sun的指导的意思了吗?

 

在这些介绍后,来看看问题的症结:剩下的两个选择都不是在任何情况下都正确的。有人认为线程类加载器应该编程新的标准方案。然而,如果多个JVM线程通过共享数据通信时这将造成一个非常混乱的类加载图景,除非他们都使用同一个上下文加载器实例。还有,委托给当前类加载器已经是一个旧规则存在于像class字面调用(即X.class)或显式调用Class.forName()的情况中(这是为什么,顺便说下,我建议避免使用这个方法的带有一个参数的版本)。即使你做出努力尽最大程度明确只使用上下文加载器,总是会有一些代码不在你的控制之下而是委托给当前加载器。这种不受控制的混合委托策略听起来相当危险。

 

更糟糕的是,某些应用服务器设置上下文和当前类加载器为不同的加载器实例,使得有相同类路径但却没有委派机制中的父子关系。花一秒钟想想为什么这是特别可怕的。记住类加载器加载和定义一个类会有一个JVM内部的ID。如果当前类加载器加载一个类X,然后要执行一个JNDI查找Y类的某些信息,上下文类加载器可能加载Y类。这个Y类实例将不同于在当前类加载器中同名并可见的类实例。强行类型转换时将会出现加载违反约束异常。

 

这种混乱将可能在Java中继续存在一段时间。拿任意一个带有任何形式的动态资源加载的J2SE API,并试着猜猜使用哪个加载策略。这里是一个样例:

  • JNDI使用上下文类加载器
  • Class.getResource()和 Class.forName() 使用当前类加载器
  • JAXP 使用上下文类加载器 (截至 J2SE 1.4)
  • java.util.ResourceBundle 使用调用的当前类加载器
  • 通过java.protocol.handler.pkgs系统属性指定的URL协议处理器只在引导类加载器和系统类加载器中查询
  • Java序列化API缺省使用调用者的当前类加载器

这些类和资源加载策略肯定是J2SE中的最不良记录。

 

一个Java程序员要做什么?

 

如果你的实现被限定于一个确定的有明确资源加载规则的框架,坚持他们。我们希望,使它们工作的负担在实现框架的人上(如应用服务器厂商,尽管他们并不总是正确的)。例如,在一个Web应用或EJB中,只要使用Class.getResource()。

 

在别的情况下,你可能会考虑使用一个解决方案,我发现在个人工作中很有用。下面的类作为一个全局决策点,用于获取应用程序任何给定时间中最佳的类加载器(所有的示例代码可以从download下载):

Java代码  收藏代码
  1. public abstract class ClassLoaderResolver   
  2.     {   
  3.         /** 
  4.          * 这个方法提供给调用此方法的人选择用于类/资源加载的最佳类加载器的实例。 
  5.          * 通常涉及JVM中调用者当前类加载器、线程上下文类加载器、系统类加载器和其他类 
  6.          * 加载器之间的选择。该加载器实例由setStrategy方法设置的IClassLoadStrategy的 
  7.          * 实例提供。 
  8.          *  
  9.          * @返回类加载器实例给调用者 [返回null表示JVM的启动类加载器]    
  10.          */   
  11.         public static synchronized ClassLoader getClassLoader ()   
  12.         {   
  13.             final Class caller = getCallerClass (0);   
  14.             final ClassLoadContext ctx = new ClassLoadContext (caller);   
  15.                
  16.             return s_strategy.getClassLoader (ctx);    
  17.         }   
  18.         public static synchronized IClassLoadStrategy getStrategy ()   
  19.         {   
  20.             return s_strategy;   
  21.         }   
  22.         public static synchronized IClassLoadStrategy setStrategy (final IClassLoadStrategy strategy)   
  23.         {   
  24.             final IClassLoadStrategy old = s_strategy;   
  25.             s_strategy = strategy;   
  26.                
  27.             return old;   
  28.         }   
  29.                
  30.         /** 
  31.          * 一个获取调用者上下文的帮助类。getClassContext()方法对 
  32.          * SecurityManager子类可见。只需要创建一个CallerResolver类的实例 
  33.          * 不必安装一个实际的安全管理器 
  34.          */   
  35.         private static final class CallerResolver extends SecurityManager   
  36.         {   
  37.             protected Class [] getClassContext ()   
  38.             {   
  39.                 return super.getClassContext ();   
  40.             }   
  41.                
  42.         } // 嵌套类结束  
  43.            
  44.            
  45.         /* 
  46.          * 获取指定偏移量位置的当前方法调用者上下文 
  47.          */   
  48.         private static Class getCallerClass (final int callerOffset)   
  49.         {            
  50.             return CALLER_RESOLVER.getClassContext () [CALL_CONTEXT_OFFSET +   
  51.                 callerOffset];   
  52.         }   
  53.            
  54.         private static IClassLoadStrategy s_strategy; //类装载时初始化(见下面的静态语句块)  
  55.            
  56.         private static final int CALL_CONTEXT_OFFSET = 3// 如果这个类重新设计时可能需要改变这个值  
  57.         private static final CallerResolver CALLER_RESOLVER; // 类装载时初始化(见下面的静态语句块)  
  58.            
  59.         static   
  60.         {   
  61.             try   
  62.             {   
  63.                 //如果当前安全管理器没有("createSecurityManager")运行时权限则可能会失败:   
  64.                    
  65.                 CALLER_RESOLVER = new CallerResolver ();   
  66.             }   
  67.             catch (SecurityException se)   
  68.             {   
  69.                 throw new RuntimeException ("ClassLoaderResolver: could not create CallerResolver: " + se);   
  70.             }   
  71.                
  72.             s_strategy = new DefaultClassLoadStrategy ();   
  73.         }   
  74. }   // 类定义结束  

通过ClassLoaderResolver.getClassLoader()静态方法获得一个类加载器的引用,可以用这个结果通过一般的类加载器API加载类和资源。另外,你可以用ResourceLoader作为类加载器的简易替换:

Java代码  收藏代码
  1. public abstract class ResourceLoader    
  2. {    
  3.     /**  
  4.      * @see java.lang.ClassLoader#loadClass(java.lang.String)  
  5.      */    
  6.     public static Class loadClass (final String name)    
  7.         throws ClassNotFoundException    
  8.     {    
  9.         final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);    
  10.                 
  11.             return Class.forName (name, false, loader);    
  12.         }    
  13.         /**  
  14.          * @see java.lang.ClassLoader#getResource(java.lang.String)  
  15.          */        
  16.         public static URL getResource (final String name)    
  17.         {    
  18.             final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);    
  19.                 
  20.             if (loader != null)    
  21.                 return loader.getResource (name);    
  22.             else    
  23.                 return ClassLoader.getSystemResource (name);    
  24.         }    
  25.         ... more methods ...    
  26.     } // 类定义结束  

决定使用何种类加载器的策略由IClassLoadStrategy 接口实现的,这是一个可插拔的组件:

Java代码  收藏代码
  1. public interface IClassLoadStrategy    
  2. {    
  3.     ClassLoader getClassLoader (ClassLoadContext ctx);    
  4. // 接口定义结束  

为了帮助IClassLoadStrategy 做决定,需要传入一个ClassLoadContext 对象:

Java代码  收藏代码
  1. public class ClassLoadContext    
  2. {    
  3.     public final Class getCallerClass ()    
  4.     {    
  5.         return m_caller;    
  6.     }    
  7.         
  8.     ClassLoadContext (final Class caller)    
  9.     {    
  10.             m_caller = caller;    
  11.         }    
  12.             
  13.         private final Class m_caller;    
  14. // 类定义结束  

ClassLoadContext.getCallerClass()返回类给ClassLoaderResolver或ResourceLoader使用。以便实现策略可以返回调用者的类加载器(上下文加载器总是可以通过Thread.currentThread().getContextClassLoader()来获取)。需要注意的是调用者是不可变的,因此,我的API不需要现有业务方法增加额外的Class 参数,同样也可用于静态方法和初始化方法。你可以根据你的部署情况添加其它属性扩展这个context对象。

 

所有这些看起像设计模式中的策略模式。核心思想是将“使用上下文类加载器”和“使用当前类加载器”的决策同你的其它具体实现逻辑分开。我们很难提前预知哪个策略是正确的,这种设计,你可以随时改变策略。

我有一个默认策略实现可以在现实工作95%的情况下正确工作:

Java代码  收藏代码
  1. public class DefaultClassLoadStrategy implements IClassLoadStrategy    
  2. {    
  3.     public ClassLoader getClassLoader (final ClassLoadContext ctx)    
  4.     {    
  5.         final ClassLoader callerLoader = ctx.getCallerClass ().getClassLoader ();    
  6.         final ClassLoader contextLoader = Thread.currentThread ().getContextClassLoader ();    
  7.             
  8.         ClassLoader result;    
  9.             
  10.             // 如果调用者加载器和上下文加载器是父子关系,则一直选择子加载器:    
  11.                 
  12.             if (isChild (contextLoader, callerLoader))    
  13.                 result = callerLoader;    
  14.             else if (isChild (callerLoader, contextLoader))    
  15.                 result = contextLoader;    
  16.             else    
  17.             {    
  18.                 // else分支可以被合并到前一个,单独列出来是要强调在模棱两可的情况下:  
  19.                 result = contextLoader;    
  20.             }    
  21.                 
  22.             final ClassLoader systemLoader = ClassLoader.getSystemClassLoader ();    
  23.                 
  24.             // 部署时作为启动类或启动扩展类的注意事项:    
  25.             if (isChild (result, systemLoader))    
  26.                 result = systemLoader;    
  27.                 
  28.             return result;    
  29.         }    
  30.             
  31.         ... more methods ...    
  32. // 类定义结束  

上面的逻辑理解起来很简单。如果调用者当前加载器和上下文加载器是父子关系,则一直选择子类加载器。子类加载器可见的资源通常也是父类加载器可见的,只要遵循J2SE的代理委托规则,大部分情况下就是正确的策略。

 

当前加载器和上下文加载器不是父子关系时不可能给出正确的策略。理想情况下,Java运行时不应该允许这种模棱两可的状况。一旦出现这种情形,我的代码就选择上下文加载器:这个策略基于本人大部分时间正确工作的经验。你可以根据需要修改代码。上下文加载器可能是框架组件中更好的选择,当前加载器可能是业务逻辑中更好的选择。

 

最后,一个简单的检查保证所选的类加载器不是系统类加载器的父加载器。如果你正在编写的代码可能部署为标准扩展库时这是个好习惯。

 

请注意我故意没检查资源或被加载的类的名称。如果不出意外,将变成J2SE核心的一部分的Java XML API的经验告诉你根据类名过滤不是一个好主意。我也没试验类的加载看看哪个加载器先成功加载。从根本上说检查类加载器的父子关系是一个更好和更可预测的方法。

 

虽然Java资源加载仍然是一个深奥的话题,随着版本的升级J2SE越来越多的依赖于各种加载策略。如果这块不给出一些有显著改进的设计方案Java将有很大的麻烦。不管你赞同还是不赞同,非常感谢你的反馈和来自个人设计经验的指正。

 

关于作者

Vladimir Roubtsov拥有超过13年的各种语言编程经验,1995年开始使用Java。现在,他作为Trilogy公司的高级工程师开发企业应用软件

 

转自 http://tyrion.iteye.com/blog/1958814

分享到:
评论

相关推荐

    Java类加载器的详解

    Java类加载器的详解,说的很详细,很不错。有兴趣的朋友可以下下来看看。

    深入Java虚拟机_002_深入详解JVM之类加载器深度剖析、根、扩展及系统类加载器

    深入Java虚拟机_002_深入详解JVM之类加载器深度剖析、根、扩展及系统类加载器

    classloader类加载器_基于java类的加载方式详解

    下面小编就为大家带来一篇classloader类加载器_基于java类的加载方式详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    java 类加载与自定义类加载器详解

    本文主要介绍了java 类加载与自定义类加载器。具有一定的参考价值,下面跟着小编一起来看下吧

    Java 类加载机制详解

    一、类加载器  类加载器(ClassLoader),顾名思义,即加载类的东西。在我们使用一个类之前,JVM需要先将该类的字节码文件(.class文件)从磁盘、网络或其他来源加载到内存中,并对字节码进行解析生成对应的Class...

    深入理解java类加载机制

    此外,我们还会探讨Java程序的类加载器和双亲委派机制,以及自定义类加载器和类卸载的实现原理和应用方法。 总的来说,本资源将为Java程序员提供全面的Java字节码和类加载原理和实践经验。通过学习本资源,开发人员将...

    详解JAVA类加载机制(推荐)

    JAVA源码编译由三个过程组成: 1、源码编译机制。 2、类加载机制 ...系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行

    JAVA-JVM-01类加载机制

    java中JVM类加载器和双亲委派机制剖析,类加载示例、加载器示例、自定义一个类加载器示例;Tomcat自定义加载器详解

    java启动过程探索 java启动过程详解

    java启动过程探索 喜欢Java,却苦于不知如何发布自己的应用,限制因素...首先,要明白jre6\bin目录下的java.exe只不过是个外壳,也叫包装器。 其的作用是 1. 方便调用 2. 完成jvm.dll的加载 3. 还有版本控制的功能

    JAVA反射机制详解视频

    (类加载器的概述和分类) (获取class文件对象的三种方式) (通过反射获取无参构造方法并使用) (通过反射获取带参构造方法并使用) (通过反射获取私有构造方法并使用) (通过反射获取成员变量并使用) (通过反射获取无参无...

    java 详解类加载器的双亲委派及打破双亲委派

    主要介绍了java 详解类加载器的双亲委派及打破双亲委派的相关资料,需要的朋友可以参考下

    java反射机制原理详解.docx

    我们创建一个类,通过编译,生成对应的.calss文件,之后使用java.exe加载(jvm的类加载器)此.class文件,此.class文件加载到内存以后,就是一个运行时类,存在缓存区,那么这个运行时类的本身就是一个class的实例 ...

    详解Android类加载ClassLoader

    基本知识 ...引导类加载器 ,用来加载Java的核心库。通过底层代码来实现的,基本上只要parent为null,那就表示引导类加载器。 比如:charsets.jar、deploy.jar、javaws.jar、jce.jar、jfr.jar、jfxswt.

    JAVA WEB 开发详解:XML+XSLT+SERVLET+JSP 深入剖析与实例应用.part2

    此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...

    JVM 运行时数据区域,垃圾回收机制,类加载机制三大功能详解.docx

    VM相关的一些内容,比如下面的这三个内容算是比较核心知识点了 运行时数据区域: 在运行时数据区里存储类Class文件元数据...类加载机制: 虚拟机首先需要把编译完成的字节码文件通过类加载器来加载到运行时数据区域

    JAVA WEB 开发详解:XML+XSLT+SERVLET+JSP 深入剖析与实例应用.part3

    此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...

    详解JVM类加载机制及类缓存问题的处理方法

    主要给大家介绍了关于JVM类加载机制及类缓存问题的处理方法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。

    JAVA WEB 开发详解:XML+XSLT+SERVLET+JSP 深入剖析与实例应用.part4

    此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...

    JAVA WEB 开发详解:XML+XSLT+SERVLET+JSP 深入剖析与实例应用.part5

    此外,本书的配套光盘还免费提供了价值人民币330元的java教学视频,对java语言进行了全面讲解,帮助一些不会java语言的读者快速地从java基础知识的学习中过渡到java web的学习与开发上. 第1部分 xml篇. 第1章 xml...

Global site tag (gtag.js) - Google Analytics