类加载器

2019-09-14  本文已影响0人  TomyZhang

一、概念

类加载器用来加载Java类到Java虚拟机中。一般来说,Java虚拟机使用Java类的方式为:Java源程序(.java文件)在经过Java编译器编译之后就被转换成Java字节代码(.class文件)。类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例,每个这样的实例用来表示一个Java类,通过此实例的newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。

二、java.lang.ClassLoader

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等。

加载类的相关方法:

getParent():返回该类加载器的父类加载器。
loadClass(String name):加载名称为name的类,返回的结果是java.lang.Class类的实例。
findClass(String name):查找名称为name的类,返回的结果是java.lang.Class类的实例。
findLoadedClass(String name):查找名称为name的已经被加载过的类,返回的结果是java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len):把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。这个方法被声明为final的。
resolveClass(Class<?> c):链接指定的Java类。

三、类加载器的树状组织结构

Java中的类加载器大致可以分成两类,一类是系统提供的,另一类是Java应用开发人员编写的。系统提供的类加载器主要有下面三个:

除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

类加载器的树状组织结构

类加载器的树状组织结构例子:

//测试代码
public class MainTest {
   public static void main(String[] args) { 
       ClassLoader loader = MainTest.class.getClassLoader(); 
       while (loader != null) { 
           System.out.println(loader.toString()); 
           loader = loader.getParent(); 
       } 
   }
}

//输出log
sun.misc.Launcher$AppClassLoader@33909752
sun.misc.Launcher$ExtClassLoader@42a57993

//分析
sun.misc.Launcher$AppClassLoader是系统类加载器,
sun.misc.Launcher$ExtClassLoader是扩展类加载器,
由于引导类加载器是由C语言编写的,在Java中并没有一个实际的类来表示它,因此返回值为null。

自定义类加载器例子1:

//测试代码
public class MainTest {
   public static void main(String[] args) { 
      String classDataRootPath = "D:\\Eclipse"; //自定义类加载路径
      FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); //自定义类加载器1
      FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); //自定义类加载器2
      String className = "com.tomorrow.test.Apple";
      try { 
          Class<?> class1 = fscl1.loadClass(className); //自定义类加载器1加载的类实例1
          System.out.println(class1.getClassLoader()); 
          System.out.println(class1);
          Object obj1 = class1.newInstance(); //通过类实例1创建的对象1
          System.out.println(obj1);
          
          Class<?> class2 = fscl2.loadClass(className); //自定义类加载器2加载的类实例2
          System.out.println(class2.getClassLoader());
          System.out.println(class2);
          Object obj2 = class2.newInstance(); //通过类实例2创建的对象2
          System.out.println(obj2);
          
          Method method = class1.getMethod("setApple", java.lang.Object.class); //获取类实例1的setApple方法
          method.invoke(obj1, obj2); //调用对象1(属于类实例1)的setApple方法,传递的参数为对象2(属于类实例2)
          System.out.println("method execute done");
      } catch (Exception e) {
          e.printStackTrace();
          System.out.println("exception happen");
      } 
   }
}

//FileSystemClassLoader
public class FileSystemClassLoader extends ClassLoader { 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       System.out.println(path);
       InputStream in = null;
       try { 
           in = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = in.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } finally {
           if(in != null) {
               try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
           }
       }
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace('.', File.separatorChar) + ".class"; 
   } 
}

//D:\Eclipse\com\tomorrow\test\Apple.class
//Apple.class对应的Apple.java代码如下:(Apple.class跟Apple.java文件已从项目目录移除掉了)
package com.tomorrow.test;

public class Apple {
    private Apple apple;
    
    public void setApple(Object apple) {
        this.apple = (Apple)apple; //类型转换
    }
}

//输出log
D:\Eclipse\com\tomorrow\test\Apple.class
com.tomorrow.test.FileSystemClassLoader@75b84c92 //类实例1的类加载器1
class com.tomorrow.test.Apple
com.tomorrow.test.Apple@232204a1
D:\Eclipse\com\tomorrow\test\Apple.class
com.tomorrow.test.FileSystemClassLoader@4aa298b7 //类实例2的类加载器2
class com.tomorrow.test.Apple
com.tomorrow.test.Apple@28d93b30
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.tomorrow.test.MainTest.main(MainTest.java:25)
Caused by: java.lang.ClassCastException: com.tomorrow.test.Apple cannot be cast to com.tomorrow.test.Apple
    at com.tomorrow.test.Apple.setApple(Apple.java:7)
    ... 5 more
exception happen

自定义类加载器例子2:

//测试代码
public class MainTest {
   public static void main(String[] args) { 
      String classDataRootPath = "D:\\Eclipse"; //自定义类加载路径
      FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); //自定义类加载器
      String className = "com.tomorrow.test.Apple";
      try { 
          Class<?> class1 = fscl1.loadClass(className); //自定义类加载器加载的类实例1
          System.out.println(class1.getClassLoader());
          System.out.println(class1);
          Object obj1 = class1.newInstance(); //通过类实例1创建的对象1
          System.out.println(obj1);
          
          Class<?> class2 = fscl1.loadClass(className); //自定义类加载器加载的类实例2
          System.out.println(class2.getClassLoader());
          System.out.println(class2);
          Object obj2 = class2.newInstance(); //通过类实例2创建的对象2
          System.out.println(obj2);
          
          Method method = class1.getMethod("setApple", java.lang.Object.class); //获取类实例1的setApple方法
          method.invoke(obj1, obj2); //调用对象1(属于类实例1)的setApple方法,传递的参数为对象2(属于类实例2)
                                     //由于类实例1跟类实例2是由同一个类加载器加载的,因此类实例1跟类实例2是相同的。
          System.out.println("method execute done");
      } catch (Exception e) {
          e.printStackTrace();
          System.out.println("exception happen");
      } 
   }
}

//FileSystemClassLoader
public class FileSystemClassLoader extends ClassLoader { 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       System.out.println(path);
       InputStream in = null;
       try { 
           in = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = in.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } finally {
           if(in != null) {
               try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
           }
       }
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace('.', File.separatorChar) + ".class"; 
   } 
}

//D:\Eclipse\com\tomorrow\test\Apple.class
//Apple.class对应的Apple.java代码如下:(Apple.class跟Apple.java文件已从项目目录移除掉了)
package com.tomorrow.test;

public class Apple {
    private Apple apple;
    
    public void setApple(Object apple) {
        this.apple = (Apple)apple; //类型转换
    }
}

//输出log
D:\Eclipse\com\tomorrow\test\Apple.class
com.tomorrow.test.FileSystemClassLoader@75b84c92 //类实例1的类加载器
class com.tomorrow.test.Apple
com.tomorrow.test.Apple@232204a1
com.tomorrow.test.FileSystemClassLoader@75b84c92 //类实例2的类加载器
class com.tomorrow.test.Apple
com.tomorrow.test.Apple@4aa298b7
method execute done

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

自定义类加载器FileSystemClassLoader的findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。

Java虚拟机判定两个Java类是否相同:
Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否相同,只有两者都相同的情况下才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类实例也是不相同的。

四、加载类的过程

类加载器在加载类时使用了代理模式,首先会代理给父类加载器来尝试加载类,如果父类加载器无法加载类然后会在子类加载器来尝试加载类。这就意味着真正完成类的加载工作的类加载器和启动类的加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的,而启动类的加载过程是通过调用loadClass来实现的。方法loadClass()抛出的是java.lang.ClassNotFoundException异常,方法defineClass()抛出的是java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的java.lang.Class类的实例缓存起来,下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次。

代理模式是为了保证Java核心库的类型安全。所有Java应用都至少需要引用java.lang.Object类,也就是说,在运行的时候java.lang.Object这个类需要被加载到Java虚拟机中。如果这个加载过程由Java应用自己的类加载器来完成的话,很可能就存在多个版本的java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于Java核心库的类的加载工作由引导类加载器来统一完成,保证了Java应用所使用的都是同一个版本的Java核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间。

五、线程上下文类加载器

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

类加载器的代理模式并不能解决Java应用开发中会遇到的类加载器的全部问题。Java提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。这些SPI接口由Java核心库来提供,而实现代码很可能是作为Java应用所依赖的jar包被包含进来,可以通过类路径(CLASSPATH)来找到,SPI接口中的代码经常需要加载具体的实现类。而问题在于,SPI接口是Java核心库的一部分,是由引导类加载器来加载的,SPI的实现类一般是由系统类加载器来加载的,引导类加载器是无法找到SPI的实现类的,因为它只加载Java的核心库,它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java应用的线程的上下文类加载器默认就是系统上下文类加载器。在SPI接口的代码中使用线程上下文类加载器,就可以成功加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。

六、Class.forName

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:

例子1:

//测试代码
public class MainTest {
   public static void main(String[] args) { 
      String classDataRootPath = "D:\\Eclipse"; 
      FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); //自定义类加载器  
      String className = "com.tomorrow.test.Apple";
      try { 
          Class<?> class1 = Class.forName(className, true, fscl1); //加载类
          System.out.println(class1.getClassLoader());
          System.out.println(class1);
      } catch (Exception e) {
          e.printStackTrace();
          System.out.println("exception happen");
      } 
   }
}

//FileSystemClassLoader
public class FileSystemClassLoader extends ClassLoader { 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       System.out.println(path);
       InputStream in = null;
       try { 
           in = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = in.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } finally {
           if(in != null) {
               try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
           }
       }
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace('.', File.separatorChar) + ".class"; 
   } 
}

//D:\Eclipse\com\tomorrow\test\Apple.class
//Apple.class对应的Apple.java代码如下:(Apple.class跟Apple.java文件已从项目目录移除掉了)
package com.tomorrow.test;

public class Apple {
    private Apple apple;
    
    static {
        System.out.println("class initialize");
    }
    
    public void setApple(Object apple) {
        this.apple = (Apple)apple;
    }
}

//输出log
D:\Eclipse\com\tomorrow\test\Apple.class
class initialize
com.tomorrow.test.FileSystemClassLoader@75b84c92
class com.tomorrow.test.Apple

例子2:

//测试代码(Apple.class跟Apple.java文件已从项目目录移除掉了)
public class MainTest {
   public static void main(String[] args) { 
      String className = "com.tomorrow.test.Apple";
      try { 
          Class<?> class1 = Class.forName(className); //加载类
          System.out.println(class1.getClassLoader());
          System.out.println(class1);
      } catch (Exception e) {
          e.printStackTrace();
          System.out.println("exception happen");
      } 
   }
}

//输出log
java.lang.ClassNotFoundException: com.tomorrow.test.Apple
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Unknown Source)
    at com.tomorrow.test.MainTest.main(MainTest.java:9)
exception happen
上一篇下一篇

猜你喜欢

热点阅读