静态库、动态连接库

程序编制一般需经编辑、编译、连接、加载和运行几个步骤。在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在连接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中。这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。

为了克服这个缺点可以采用动态连接库。这个时候连接器仅仅是在可执行文件中打上标志,说明需要使用哪些动态连接库;当运行程序时,加载器根据这些标志把所需的动态连接库加载到内存。

另外在当前的编程环境中,一般都提供方法让程序在运行的时候把某个特定的动态连接库加载并运行,也可以将其卸载(例如Win32的LoadLibrary()&FreeLibrary()和Posix的dlopen()&dlclose())。这个功能被广泛地用于在程序运行时刻更新某些功能模块或者是程序外观。

What is ClassLoader?

与普通程序不同的是,Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。


  类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。


ClassLoader 的基本目标是对类的请求提供服务。当 JVM 需要使用类时,它根据名称向 ClassLoader 请求这个类,然后 ClassLoader 试图返回一个表示这个类的 Class 对象。 通过覆盖对应于这个过程不同阶段的方法,可以创建定制的 ClassLoader。


Thread.currentThread().getContextClassLoader()指的是取得当前线程的ClassLoader,而Class.forName()其实也是使用当前线程的ClassLoader装入一个类,二者并没有本质的区别。
Class.forName()只是为了方便装入类而写的一个静态方法,不知为何Banq总是拿来和ClassLoader作比较,二者不是同一个级别的东西。
通过调用Thread.currentThread().setContextClassLoader(ClassLaoder load)方法,Class.forName()也可以装入其他ClassLoader范围内的类。


JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,Bootstrap ClassLoader是用本地代码实现的,它负责加载核心Java Class(即所有java.*开头的类)。另外JVM还会提供两个ClassLoader,它们都是用Java语言编写的,由Bootstrap ClassLoader加载;其中Extension ClassLoader负责加载扩展的Java class(例如所有javax.*开头的类和存放在JRE的ext目录下的类),Application ClassLoader负责加载应用程序自身的类。


Classloader存在下面问题:
在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。如ClassLoaderA,装载了类A的类型实例A1,而ClassLoaderB,也装载了类A的对象实例A2。逻辑上讲A1
=A2,但是由于A1和A2来自于不同的ClassLoader,它们实际上是完全不同的,如果A中定义了一个静态变量c,则c在不同的ClassLoader中的值是不同的。




When to load the class?

什么时候JVM会使用ClassLoader加载一个类呢?当你使用java去执行一个类,JVM使用Application ClassLoader加载这个类;然后如果类A引用了类B,不管是直接引用还是用Class.forName()引用,JVM就会找到加载类A的ClassLoader,并用这个ClassLoader来加载类B。

 

2, 一些重要的方法
A)  方法 loadClass 
        ClassLoader.loadClass() 是 ClassLoader 的入口点。该方法的定义如下: 
        Class loadClass( String name, 
boolean resolve );
         name  JVM 需要的类的名称,如 Foo 或 java.lang.Object。
         resolve 参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。并不总是需要解析。如果 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要解析。 
     
    B)  方法 defineClass 
       defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。defineClass 管理 JVM 的许多复杂、神秘和倚赖于实现的方面 
-- 它把字节码分析成运行时数据结构、校验有效性等等。不必担心,您无需亲自编写它。事实上,即使您想要这么做也不能覆盖它,因为它已被标记成final的。 

    C)  方法 findSystemClass 
       findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。(Java 
2 中 ClassLoader 的变动提供了关于 Java 版本 1.2 这个过程变动的详细信息。) 对于定制的 ClassLoader,只有在尝试其它方法装入类之后,再使用 findSystemClass。原因很简单:ClassLoader 是负责执行装入类的特殊步骤,不是负责所有类。例如,即使 ClassLoader 从远程的 Web 站点装入了某些类,仍然需要在本地机器上装入大量的基本 Java 库。而这些类不是我们所关心的,所以要 JVM 以缺省方式装入它们:从本地文件系统。这就是 findSystemClass 的用途。 

     D) 方法 resolveClass 
   正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值。 


   E) 方法 findLoadedClass 
      findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。


怎么组装这些方法
  1) 调用 findLoadedClass 来查看是否存在已装入的类。
  2) 如果没有,那么采用那种特殊的神奇方式来获取原始字节。
  3) 如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。
  4) 如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
  5) 如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
  6) 如果还没有类,返回 ClassNotFoundException。



看看jdk中classLoader的代码:

 

 protected synchronized Class<?> loadClass(String name, boolean resolve)
    
throws ClassNotFoundException
    
{
    
// First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    
if (c == null{
        
try {
        
if (parent != null{
            c 
= parent.loadClass(name, false);
        }
 else {
            c 
= findBootstrapClass0(name);
        }

        }
 catch (ClassNotFoundException e) {
            
// If still not found, then invoke findClass in order
            
// to find the class.
            c = findClass(name);
        }

    }

    
if (resolve) {
        resolveClass(c);
    }

    
return c;



1. 调用findLoadedClass(String):Class 检查一下这个class是否已经被加载过了,由于JVM 规范规定ClassLoader可以cache它所加载的Class,因此如果一个class已经被加载过的话,直接从cache中获取即可。
2. 调用它的parent 的loadClass()方法,如果parent为空,这使用JVM内部的class loader(即著名的bootstrap classloader)。
3. 如果上面两步都没有找到,调用findClass(String)方法。

Why use your own ClassLoader?

似乎JVM自身的ClassLoader已经足够了,为什么我们还需要创建自己的ClassLoader呢?

因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,如果编写你自己的ClassLoader,你可以做到:
1)在执行非置信代码之前,自动验证数字签名
2)动态地创建符合用户特定需要的定制化构建类
3)从特定的场所取得java class,例如数据库中
4) 等等

事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息。

目前的应用服务器大都使用了ClassLoader技术,即使你不需要创建自己的ClassLoader,了解其原理也有助于更好地部署自己的应用。 

ClassLoader Tree & Delegation Model

当你决定创建你自己的ClassLoader时,需要继承java.lang.ClassLoader或者它的子类。在实例化每个ClassLoader对象时,需要指定一个父对象;如果没有指定的话,系统自动指定ClassLoader.getSystemClassLoader()为父对象。如下图:

在Java 1.2后,java class的加载采用所谓的委托模式(Delegation Modle),当调用一个ClassLoader.loadClass()加载一个类的时候,将遵循以下的步骤:
1)检查这个类是否已经被加载进来了?
2)如果还没有加载,调用父对象加载该类
3)如果父对象无法加载,调用本对象的findClass()取得这个类。

所以当创建自己的Class Loader时,只需要重载findClass()这个方法。


publicclass AnotherClassLoader extends ClassLoader {

    
private String baseDir;
    privatestaticfinal Logger LOG 
= Logger.getLogger(AnotherClassLoader.class); 
    
    
public AnotherClassLoader (ClassLoader parent, String baseDir){
        
super(parent);
        
this.baseDir = baseDir;    
        }

    
protected Class findClass(String name)throws ClassNotFoundException {
        LOG.debug(
"findClass " + name);
        
byte[] bytes = loadClassBytes(name);       
        Class theClass 
= defineClass(name, bytes, 0, bytes.length);//A
        if (theClass == null)
            thrownew ClassFormatError();
return theClass;    
        }

        
        
   
private byte[] loadClassBytes(String className) throws ClassNotFoundException {
       
try {            
           String classFile 
= getClassFile(className);            
           FileInputStream fis 
= new FileInputStream(classFile);           
           FileChannel fileC 
= fis.getChannel();           
           ByteArrayOutputStream baos 
= new ByteArrayOutputStream();           
           WritableByteChannel outC 
= Channels.newChannel(baos);         
           ByteBuffer buffer 
= ByteBuffer.allocateDirect(1024);
           
while (true{
               
int i = fileC.read(buffer);
               
if (i == 0 || i == -1
                   
break;                              
               buffer.flip();                
               outC.write(buffer);               
               buffer.clear();            
               }
            
           fis.close();
           
return baos.toByteArray();        }

      
catch (IOException fnfe) {
               thrownew ClassNotFoundException(className);      
               }
    }

     
private String getClassFile(String name) {
         StringBuffer sb 
= new StringBuffer(baseDir);     
         name 
= name.replace('.', File.separatorChar) + ".class";      
         sb.append(File.separator 
+ name);
         
return sb.toString();    
         }

}



很简单的代码,关键的地方就在A处,我们使用了defineClass方法,目的在于把从class文件中得到的二进制数组转换为相应的Class实例。defineClass是一个native的方法,它替我们识别class文件格式,分析读取相应的数据结构,并生成一个class实例。

还没完呢,我们只是找到了发布在某个目录下的class,还有资源呢。我们有时会用Class.getResource():URL来获取相应的资源文件。如果仅仅使用上面的ClassLoader是找不到这个资源的,相应的返回值为null。

public java.net.URL getResource(String name) {    
    name 
= resolveName(name);        
    ClassLoader cl 
= getClassLoader0();//这里
    if (cl==null)
    
{// Asystem class.
    return ClassLoader.getSystemResource(name);        }

    
return cl.getResource(name);
    }
原来是使用加载这个class的那个classLoader获取得资源。


public URL getResource(String name) {    
    URL url;
    
if (parent != null{       
        url 
= parent.getResource(name);    }

   
else {        
       url 
= getBootstrapResource(name);    }

if (url == null{        
    url 
= findResource(name);//这里    }return url;
    }

这样看来只要继承findResource(String)方法就可以了。修改以下我们的代码:


//新增的一个findResource方法
protected URL findResource(String name) {       
    LOG.debug(
"findResource " + name);
    
try {         
        URL url 
= super.findResource(name);
        
if (url != null)
            
return url;          
        url 
= new URL("file:///" + converName(name));//简化处理,所有资源从文件系统中获取
        return url;        }
 
    
catch (MalformedURLException mue) 
        LOG.error(
"findResource", mue);
        
return null;        }

        }

        
private String converName(String name) {   
    StringBuffer sb 
= new StringBuffer(baseDir);   
    name 
= name.replace('.', File.separatorChar);     
    sb.append(File.separator 
+ name);
    
return sb.toString();
    }





Unloading? Reloading?

当一个java class被加载到JVM之后,它有没有可能被卸载呢?我们知道Win32有FreeLibrary()函数,Posix有dlclose()函数可以被调用来卸载指定的动态连接库,但是Java并没有提供一个UnloadClass()的方法来卸载指定的类。

在Java中,java class的卸载仅仅是一种对系统的优化,有助于减少应用对内存的占用。既然是一种优化方法,那么就完全是JVM自行决定如何实现,对Java开发人员来说是完全透明的。

在什么时候一个java class/interface会被卸载呢?Sun公司的原话是这么说的:"class or interface may be unloaded if and only if its class loader is unreachable. Classes loaded by the bootstrap loader may not be unloaded."

事实上我们关心的不是如何卸载类的,我们关心的是如何更新已经被加载了的类从而更新应用的功能。JSP则是一个非常典型的例子,如果一个JSP文件被更改了,应用服务器则需要把更改后的JSP重新编译,然后加载新生成的类来响应后继的请求。

其实一个已经加载的类是无法被更新的,如果你试图用同一个ClassLoader再次加载同一个类,就会得到异常(java.lang.LinkageError: duplicate class definition),我们只能够重新创建一个新的ClassLoader实例来再次加载新类。至于原来已经加载的类,开发人员不必去管它,因为它可能还有实例正在被使用,只要相关的实例都被内存回收了,那么JVM就会在适当的时候把不会再使用的类卸载。