Java类加载器(Class loader)是一个很重要的概念,一直想写一篇关于这个的博客,今天看了不少别人的博客,也来写一下,希望能写的明白。
首先明白类加载器的概念:
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class
类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()
方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。例如:当我们
有一个A类,先是javac.A.java,生成了A.class文件,这个时候,再java A ,此时jvm就需要加载A类,它只得向类加载器要,此时类加载器用处的时候就来了,将A类加载给jvm,具体怎么加载后面介绍。
明白了类加载器的基本概念,我们就会想在java中一切都是对象,类加载器也是一个java类,那么谁来加载它呢?--->jvm
jvm在启动时,会启动jre/rt.jar里的类加载器:bootstrap classloader,用来加载java核心api;然后启动扩展类加载器ExtClassLoader加载扩展类,并加载用户程序加载器AppClassLoader,并指定ExtClassLoader为他的父类;
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用c++来实现的,并不继承自
java.lang.ClassLoader
。 - 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()
来获取它。
系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器,在java中使用的是双亲委托模式,-->加载一个类时,首先BootStrap进行寻找,找不到再由Extension ClassLoader寻找,最后才是App ClassLoader,这样,可以有效避免安全问题。
基本概念讲到这里,接下来我们看一下java.lang.ClassLoader这个类:ClassLoader为一个抽象类,由前面可知该类是由bootstrap ClassLoader加载,所有的类加载器器实现类(除了bootstrap ClassLoader)都需要继承该类,
getParent() |
返回该类加载器的父类加载器。如果该类的父类加载器为bootstrap classloader则返回null |
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 类。 |
- // First, check if the class has already been loaded
- //JVM 规范规定ClassLoader可以在缓存保留它所加载的Class,如果一个Class已经被加载过,则直接从缓存中获取
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;
}
//如果没有被加载过 则调用findBootstrapClass0()方法,官方原文:
- Invoke
findLoadedClass(String)
to check if the class has already been loaded. - Invoke the
loadClass
method on the parent class loader. If the parent is null the class loader built-in to the virtual machine is used, instead. - Invoke the
findClass(String)
method to find the class.
从上面可以看出:类加载器加载一个类,首先通过类名去判别该类是否被加载过,如果被加载过,直接去缓存中取,如果没有被加载过,则用系统类加载器去加载它(system class loader),没有加载成功,调用findBootstrapClass0(name)方法加载,如果两个都没有成功,则调用findclass(String name)方法再找该类。
线程上下文类加载器:
在Thread类里面有两个方法:getContextClassLoader(),setContextClassLoader(cl);分别用来获取和设置线程的上下文类加载器器,如果没有通过setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。(sun.misc.Launcher$AppClassLoader)在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers
包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory
类中的 newInstance()
方法用来生成一个新的DocumentBuilderFactory
的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个
this.getClass.getClassLoader();
Thread.currentThread().getContextClassLoader();
方法一得到的Classloader是静态的,表明类的载入者是谁;方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。
Class.forName
Class.forName
是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一种形式的参数 name
表示的是类的全名;initialize
表示是否初始化类;loader
表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize
的值为 true
,loader
的值为当前类的类加载器。Class.forName
的一个很常见的用法是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用来加载 Apache Derby 数据库的驱动。
/********基本概念到此为止,接下来开发自己的类加载器******************/
首先我们需要知道为什么需要开发自己的类加载器,。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来
我们自己写的类加载器一般重写findClass(name)这个方法即可
假设:我们有一个HelloWorld类,代码如下:
1 public class HelloWorld 2 { 3 public HelloWorld() { 4 System.out.println("Test"); 5 } 6 public static void main(String[] args) { 7 System.out.println("Hello World"); 8 } 9 }
这时候 我们不想Java的系统类加载器(system class loader)加载它,而是我们自己的类加载器加载它,则让我们的类加载器器类继承java.lang.ClassLoader类,我们自己实现
findclass()方法,步骤:
1:根据构造方法传入的类名找出自己要加载的类
2:利用java.lang.ClassLoader的findLoadedClass(name)方法,判断是否缓存中有该类,如果有直接返回,没有的话,则进行下一步
3:因为我们的目标是将我们要加载的类转换成byte[]数组,然后用this.defineClass(name, b, off, len)方法定义出类,给虚拟机所以我们朝这个方向努力即可,代码不做过多解释,里面有一些java.nio.*的类,会专门写一篇博客讲述
1 public class MyClassLoader extends ClassLoader{ 2 3 private String rootDir; 4 5 public MyClassLoader(String rootDir){ 6 this.rootDir = rootDir; 7 } 8 9 10 @Override 11 protected Class<?> findClass(String name) throws ClassNotFoundException { 12 Class<?> clazz = this.findLoadedClass(name); 13 if(clazz == null){ 14 try { 15 String classFile = nameToPath(name); 17 FileInputStream in = new FileInputStream(classFile); 18 //将目标类写入 19 FileChannel channel = in.getChannel(); 20 21 ByteArrayOutputStream out = new ByteArrayOutputStream(); 22 WritableByteChannel byteChannel = Channels.newChannel(out); 23 //定义一个人长度为1024大小的byte缓冲区 24 ByteBuffer buffer = ByteBuffer.allocate(1024); 25 while(true){ 27 int readNum = channel.read(buffer); 28 if(readNum == -1){ 30 break; 31 } 33 buffer.flip(); 34 byteChannel.write(buffer); 35 buffer.clear(); 37 } 38 39 channel.close(); 40 byte[] byteArray = out.toByteArray(); 41 clazz = this.defineClass(name, byteArray, 0, byteArray.length); 42 43 } catch (FileNotFoundException e) { 44 e.printStackTrace(); 45 } catch (IOException e) { 46 e.printStackTrace(); 47 } 49 } 50 return clazz; 51 } 52 53 54 public String nameToPath(String className){ 55 StringBuffer sb = new StringBuffer(rootDir); 56 className = className.replace(‘.‘, File.separatorChar) + ".class"; 57 sb.append(File.separator + className); 58 return sb.toString(); 59 } 60 61 }
最后测试一下我们写的类就可以了:
1 public class Test{ 2 public static void main(String[] args) { 3 try { 4 MyClassLoader myclassloader = new MyClassLoader("f:"+File.separator+"workbase"); 5 Class<?> clazz = myclassloader.findClass("HelloWorld"); 6 clazz.newInstance(); 7 System.out.println(clazz.getClassLoader()); 8 9 } catch (Exception e) { 10 e.printStackTrace(); 11 } 12 } 13 }
运行结果:[email protected],可以看出确实使用了我们自己写的类加载器。
总结:个人感觉明白这些应该就OK了吧,当然看到别人博客还有许多更加深层次的东西就不说了