sharajava

2006年7月25日 #

类的动态加载

: 调用 Class.forName() ClassLoader.loadClass() 的区别在什么地方 ?

: 这两方法都是通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象 . 尽管如此 , 它们的在行为方式上还是有区别的 .

Ø         用哪个 java.lang.ClassLoader 进行加载

Ø         返回的 Class 对象是否被初始化

Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器. 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器. 如果用特定的类加载器来加载类在你的设计中占有比较重要的地位, 你就应该调用ClassLoader.loadClass(String)方法或Class.forName(String, boolean, ClassLoader)方法.

    另外, Class.forName()方法对加载的类对象进行初始化. 可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的.

    你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化.

    最常用的是Class.forName(String, boolean, ClassLoader). 设置第二个参数为false即推迟初始化, 第三个参数指定要用来进行加载的类加载器. 我建议为了最大的灵活性使用这个方法.

类初始化错误是难处理的

    成功地加载了类, 并不意味着就不会有其它问题. 静态初始化代码可以抛出异常, 异常被包装到java.long.ExceptionInInitializerError的实例中. 异常抛出后, 这个类将不可用. 这样, 如果你需要在代码中处理这些错误, 你就应该调用进行初始化的Class.forName()方法.

    但进一步说, 如果你要处理ExceptionInInitializerError并试图从错误中恢复, 很可能不如你想象的那样正常工作. 请看下面的示例代码:


public   class  Main
{
    
public   static   void  main (String [] args)  throws  Exception
    {
        
for  ( int  repeat = 0; repeat < 3; ++ repeat)
        {
            
try
            {
                
// "Real" name for X is outer class name+$+nested class name:
                Class.forName ("Main$X");
            }
            
catch  (Throwable t)
            {
                System.out.println ("load attempt #" + repeat + ":");
                t.printStackTrace (System.out);
            }
        }
    }

    
private   static   class  X
    {
        
static
        {
            
if  (++ s_count == 1)
                
throw   new  RuntimeException ("failing static initializer");
        }
        
    } 
// End of nested class

    
private   static   int  s_count;

// End of class

    上面的代码3次尝试加载一个内部类X, 即便是X的静态初始化只在每一次加载时失败, 3次加载都抛出了异常.

>java Main
load attempt #0:
java.lang.ExceptionInInitializerError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)
Caused by: java.lang.RuntimeException: failing static initializer...
        at Main$X.<clinit>(Main.java:40)
        ... 3 more
load attempt #1:
java.lang.NoClassDefFoundError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)
load attempt #2:
java.lang.NoClassDefFoundError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:140)
        at Main.main(Main.java:17)

    有点令人吃惊的时, 在第2, 3次进行类加载时, 抛出的异常竟然是java.lang.NoClassDefFoundError. 这里发生的事情是, 第一次加载后(在进行初始化之前), JVM发现X已经被加载, 而这个X的类实例在加载它的类加载器被垃圾回收之前是不会被卸载的. 所以这之后的对Class.forName()的调用时, JVM不会再尝试进行初始化的工作, 但是, 更令人不解的是, 抛出一个NoClassDefFoundError.

    卸载这样的类的方法是丢弃原来加载该类的类加载器实例并重新创建一个. 当然, 这只能是在你使用了Class.forName(String, boolean, ClassLoader)这个3参数的方法的时候才能办到.

隐藏的 Class.forName() 方法

    你一定用过JavaX.class的语法去获取一个在编译器就知道名字的类对象实例. 在字节码的层次上, 这一点是如何实现的就不被人熟知了. 不同的编译器有不同的实例细节, 但共同点是, 所有编译器所相应产生的代码都是调用的Class.forName(String)这一个参数的方法. 比如J2SE 1.4.1javac就把Class cls = X.class; 翻译成如下等价的形式:

 
        
// This is how "Class cls = X.class" is transformed:
         if  ( class $Main$X ==  null )
        {
            
class $Main$X =  class $ ("Main$X");
        }
        Class cls = 
class $Main$X;

    

    
static  Class  class $ (String s)
    {
        
try
        {
            
return  Class.forName (s);
        }
        
catch  (ClassNotFoundException e)
        {
            
throw   new  NoClassDefFoundError (e.getMessage());
        }
    }

    
static  Class  class $Main$X;  // A synthetic field created by the compiler

Sun javac 开个玩笑

从上面的例子你可以看到, 编译器调用Class.forName()方法加载类对象, 并将其缓存到一个包内可见的静态变量中. 这种令人费解的实现方式的可能是因为在早期版本的Java, 这种X.class的语法还未被支持, so the feature was added on top of the Java 1.0 byte-code instruction set.(???)

利用这一点, 你可以在编译器的开销上做一些有趣的事情. J2SE 1.3.1编译下面的代码片段:

public   class  Main
{
    
public   static   void  main (String [] args)  throws  Exception
    {
        System.out.println ("String class: " + String.
class );
        
class $java$lang$String =  int . class ;
        System.out.println ("String class: " + String.
class );
    }
    
    
static  Class  class $java$lang$String;

// End of class

运行它, 你会得到下面这个很荒谬的输出结果:

>java Main

String class: class java.lang.String

String class: int

J2SE 1.4.1, 上面的代码将不能被编译通过, 但你仍然可以用反射的方式戏弄它:

public   static   void  main (String [] args)  throws  Exception
    {
        System.out.println ("String class: " + String.
class );
        Main.
class .getDeclaredField ("class$java$lang$String").set ( null int . class );
        System.out.println ("String class: " + String.
class );
    }

 

    综上所述, 下次你再调用Class.forName()方法时, 你应该知道它的局限性可选的替代方案了.

 

 

posted @ 2006-07-27 09:06 sharajava 阅读(2111) | 评论 (0)编辑 收藏

JBoss MBean服务加载过程

http://www.blogjava.net/images/blogjava_net/sharajava/13416/o_start-jboss-mbean-service.JPG 

 

1.      前缀1.1的方法, 代表加载和解析XML服务描述文件的过程.

2.      前缀1.2的方法, 代表对XML服务描述文件中类路径条目的处理. 这个过程创建独立的部署, 使得jar和类目录对于相应的统一类加载器(UnifiedClassLoader)可用. 这个UnifiedClassLoader是注册到统一的类加载器仓库中的.

3.      前缀1.3的方法, 代表处理服务描述文件中定义的本地目录条目的过程. 这个过程把在路径属性中指定的SAR相关条目复制一份到server/<config>/db目录下.

4.      方法1.4, 代表对已经部署的服务中嵌套的可部单元的部署过程. 子部署项目被创建并被加入到服务部署信息的子部署项列表中.

5.      方法2.1, SAR部署单元相应的UnifiedClassLoader(本身也是MBean)被注册到MBean Server, 这样它就可以被用来加载SAR中的MBean.

6.      方法2.2, 创建XML服务描述文件中定义的每个MBean并用描述文件中给定的值初始化其属性. 这些工作通过调用ServiceControllerinstall(Element, ObjectName)方法来完成的.

7.      方法2.4.1, 对于前面步骤中创建好的每个MBean实例, 获取其JMX对象名并由ServiceController处理服务生命周期中的create步骤. ServiceController处理MBean服务的依赖关系, 只有当所有依赖都满足时, create方法才会被调用.

8.      前缀3.1的方法, 代表MBean实例的起动过程. 对于创建好的每个MBean实例, 获取其JMX对象名并由ServiceController处理服务生命周期中的start步骤. ServiceController处理MBean服务的依赖关系, 只有当所有依赖都满足时, start方法才会被调用.

posted @ 2006-07-25 11:18 sharajava 阅读(1890) | 评论 (0)编辑 收藏

走出ClassLoader迷局

: 我什么时候应该使用 Thread.getContextClassLoader()?

: 这个问题经常出现在编写框架代码 , 需要动态加载很多类和资源的时候 . 通常当你需要动态加载资源的时候 , 你至少有三个 ClassLoader 可以选择 :

²        系统类加载器或叫作应用类加载器 (system classloader or application classloader)

²        当前类加载器

²        当前线程类加载器

上面的问题指的是最后一种类加载器 . 哪种类加载器是正确的选择呢 ?

第一种选择可以很容易地排除 : 系统类加载器 (system classloader). 这个类加载器处理 -classpath 下的类加载工作 , 可以通过 ClassLoader.getSystemClassLoader() 方法调用 . ClassLoader 下所有的 getSystemXXX() 的静态方法都是通过这个方法定义的 . 在你的代码中 , 你应该尽量少地调用这个方法 , 以其它的类加载器作为代理 . 否则你的代码将只能工作在简单的命令行应用中 , 这个时候系统类加载器 (system classloader) JVM 最后创建的类加载器 . 一但你把代码移到 EJB, Web 应用或 Java Web Start 应用中 , 一定会出问题 .

      所以我们来看第二种选择 : 当前上下文环境下的类加载器 . 根据定义 , 当前类加载器就是你当前方法所属的类的加载器 . 在运行时类之间动态联编 , 及调用 Class.forName,() Class.getResource() 等类似方法时 , 这个类加载器会被隐含地使用 . It is also used by syntactic constructs like X.class class literals.

    线程上下文类型加载器是在Java 2平台上被引入的. 每一个线程都有一个类加载器与之对应(除非这个线程是被本地代码创建的). 这个类加载器是通过Thread.setContextClassLoaser()方法设置的. 如果你不在线程构造后调用这个方法, 这个线程将从它的父线程中继承相应的上下文类加载器. 如果在整个应用中你不做任何特殊设置, 所有的线程将都以系统类加载器(system classloader)作为自己的线程上下文类加载器. 自从WebJ2EE应用服务器使用成熟的类加载器机制来实现诸如JNDI, 线程池, 组件热部署等功能以来, 这种在整个应用中不做任何线程类加载器设置的情况就很少了.

    为什么线程上下文类加载器存在于如此重要的位置呢? 这个概念在J2SE中的引入并不引人注目. 很多开发人员对这一概念迷惑的原因是Sun公司在这方面缺乏适当的指引和文档.

    事实上, 上下文类加载器提供了类加载机制的后门, 这一点也在J2SE中被引入了. 通常, JVM中的所有类加载器被组织成了有继承层次的结构, 每一个类加载器(除了引导JVM的原始类加载器)都有一个父加载器. 每当被请示加载类时, 类加载器都会首先请求其父类加载器, 只有当父类加载器不能加载时, 才会自己进行类加载.

   有时候这种类加载的顺序安排不能正常工作, 通常当必须动态加载应用程序开发人员提供的资源的时候. JNDI为例: 它的内容(J2SE1.3开始)就在rt.jar中的引导类中实现了, 但是这些JNDI核心类需要动态加载由独立厂商实现并部署在应用程序的classpath下的JNDI提供者. 这种情况就要求一个父classloader(本例, 就是引导类加载器)去加载对于它其中一个子classloader(本例, 系统类加载器)可见的类. 这时通常的类加载代理机制不能实现这个要求. 解决的办法(workaround)就是, JNDI核心类使用当前线程上下文的类加载器, 这样, 就基本的类加载代理机制的相反方向建立了一条有效的途径.

    另外, 上面一段可能让你想起一些其它的事情: XML解析Java API(JAXP). 是的, JAXP只是J2SE的扩展进, 它很自然地用当前类加载器来引导解析器的实现. 而当JAXP被加入到J2SE1.4的核心类库中时, 它的类加载也就改成了用当前线程类加载器, JNDI的情况完全类似(也使很多程序员很迷惑). 明白为什么我说来自Sun的指导很缺乏了吧?

   在以上的介绍之后, 我们来看关键问题: 这两种选择(当前类加载器和当前线程类加载器)都不是在所有环境下都适用. 有些人认为当前线程类加载器应该成为新的标准策略. 但是, 如果这样, 当多个线程通过共享数据进行交互的时, 将会呈现出一幅极其复杂的类加载的画面, 除非它们全部使用了同一个上下文的类加载器. 进一步说, 在某些遗留下来的解决方案中, 委派到当前类加载器的方法已经是标准. 比如对Class.forName(String)的直接调用(这也是我为什么推荐尽量避免对这个方法进行调用的原因). 即使你努力去只调用上下文相关的类加载器, 仍然会有一些代码会不由你控制. 这种不受控制的类加载委派机制是混入是很危险的.

    更严重的问题, 某些应用服务器把环境上下文及当前类加载器设置到不同的类加载器实例上, 而这些类加载器有相同的类路径但却没有委派机制中的父子关系. 想想这为什么十分可怕. 要知道类加载器定义并加载的类实例会带有一个JVM内部的ID. 如果当前类加载器加载一个类X的实例, 这个实例调用JNDI查找类Y的实例, 些时的上下文的类加载器也可以定义了加载类Y实例. 这个类Y的定义就与当前类加载器看到的类Y的定义不同. 如果进行强制类型转换, 则产生异常.

   这种混乱的情况还将在Java中存在一段时间. 对于那些需要动态加载资源的J2SEAPI, 我们来猜想它们的类加策略. 例如:

Ø         JNDI 使用线程上下文类加载器

Ø         Class.getResource() Class.forName()使用当前类加载器

Ø         JAXP(J2SE 1.4 及之后)使用线程上下文类加载器

Ø         java.util.ResourceBundle 使用调用者的当前类加载器

Ø         URL protocol handlers specified via java.protocol.handler.pkgs system property are looked up in the bootstrap and system classloaders only

Ø         Java 序列化API默认使用调用者当前的类加载器

这些类及资源的加载策略问题, 肯定是J2SE领域中文档最及说明最缺乏的部分了.

posted @ 2006-07-25 11:04 sharajava 阅读(2497) | 评论 (1)编辑 收藏