这一章我们主要是对双亲委派机制进行详细讲解:
前面我们知道类加载有系统自带的3种加载器,也有自定义的加载器,那么这些加载器之间的关系是什么,已经在加载类的时候,谁去加载呢?这节,我们将进行讲解。
一、双亲委派机制
JVM的ClassLoader采用的是树形结构,除了BootstrapClassLoader以外?每个ClassLoader都会有一个parentClassLoader,用户自定义的ClassLoader默认的parentClassLoader是SystemClassLoader,当然你可以自己指定需要用哪一个ClassLoader的实例,我们来看他的API:
默认的无参构造方法使用的是SystemClassLoader,你可以通过传入一个ClassLoader的实例来指定他的父类加载器。这里强调一点,很多人认为各个父子类加载器之间是继承关系,这里澄清一下,父子类加载器之间是组合关系,子类类加载器会含有一个parentClassLoader的对象,类加载的时候通常会按照树形结构的原则来进行,也就是说,首先是从parentClassLoader中尝试进行加载,当parent无法进行加载时,再从当前的类加载器进行加载,以此类推。JVM会保证一个类在同一个ClassLoader中只会被加载一次。
ClassLoader抽象类为我们定义了一系列的关键的方法,下来让我们来看一下
1、loadClass方法,此方法用来加载指定名字的类,ClassLoader会先从已加载的类中寻找,如果没有,则使用父加载器进行加载,如果加载成功则加载,否则从当前的类加载器中进行加载,如果还没有找到该类的class文件则会抛出异常ClassNotFoundException
如果该类需要链接,则通过resolveClass进行链接。
2、defineClass,此方法用来将二进制的字节码转换为Class对象,这个对类的自定义加载非常重要,当然前文我们已经说了,当类的二进制文件被加载到内存之后,要进行语法分析,语义分析等一系列的验证,如果不符合JVM规范,则抛出ClassFormateError错误,如果生成的类名和字节码中的不一致,则抛出NoClassDefFoundException,如果加载的class是受保护的、采用不同的标签名的,或者一java.*开头的,则抛出SecurityException,如果要加载的class在之前已经被加载过,则直接抛出LinkageError。
3、resolveClass,此方法完成Class的链接,如果链接过则直接返回。当Java开发人员调用Class.forName来获取一个class对象的时候,JVM会从方法栈上寻找第一个ClassLoader,通常也就是执行Class.forName的ClassLoader,并使用这个ClassLoader来加载此类。JVM为了保护加载、执行的类的安全,不允许ClassLoader直接卸载加载了的类,只有JVM才可以卸载,在SUN的JDK中,只有ClassLoader没有 被引用的时候,次ClassLoader加载的类才会被卸载!
附:JDK中ClassLoader的部分源码
1、 构造函数
protected ClassLoader(ClassLoader parent) { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = parent; initialized = true; } protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); initialized = true; }
2、loadClass
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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; }
类的这种加载机制我们称之为父委托加载机制,父委托机制的优点就是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载本应该由父加载器加载的可靠类,从而防止不可靠的甚至恶意的代码代替由父类加载器加载的可靠代码。如,java.lang.Object类总是由根类加载器加载的,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
被定义的类加载器,而它的父类加载器则被称为初始类加载器。
我们知道java中很可能出现类名相同的类,但是JVM却能正常的加载,是因为我们将相同的类名的类放在了不通的包(package)下面,这个也成为命名空间,每个类加载器都有自己的命名空间,命名空间是由该加载器以及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包名+类名)相同的两个类;在不同的命名空间中,有可能出现类的完整名字相同的两个类。
由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看他们的包名称是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类之间才能相互访问可见(默认访问级别)的类和成员。假设用户自定义了一个类java.lang.TestCase并由用于自定义的类加载器加载,由于java.lang.TestCase和核心类库java.lang.*由不同的类加载器加载,他们属于不同的运行时包,所以java.lang.TestCase不能访问核心库java.lang包中的包可见成员。
同一个命名空间内的类是相互可见的。
子类加载器的命名空间包含所有父类加载器的命名空间,因此由子类加载器加载的类能看见父类加载器加载的类,相反,由父类加载器加载的类不能看见子类加载器加载的类。如果两个加载器之间没有直接或者间接的父子关系,那么他们各自加载的类互不可见。
二、自定义类加载器
为什么我们要自定义类加载器?因为虽然Java中给用户提供了很多类加载器,但是和实际使用比起来,功能还是匮乏。举一个例子来说吧,主流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,要解决如下几个问题:
1、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的要求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相使用
2、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。这个需求也很常见,比如相同的Spring类库10个应用程序在用不可能分别存放在各个应用程序的隔离目录中
3、支持热替换,我们知道JSP文件最终要编译成.class文件才能由虚拟机执行,但JSP文件由于其纯文本存储特性,运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待
由于存在上述问题,因此Java提供给用户使用的ClassLoader就无法满足需求了。Tomcat服务器就有自己的ClassLoader架构,当然,还是以双亲委派模型为基础的:
双亲委派流程为:
JDK中的ClassLoader
在实现自己的ClassLoader之前,我们先看一下JDK中的ClassLoader是怎么实现的:
1 protected synchronized Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 // First, check if the class has already been loaded 5 Class c = findLoadedClass(name); 6 if (c == null) { 7 try { 8 if (parent != null) { 9 c = parent.loadClass(name, false); 10 } else { 11 c = findBootstrapClass0(name); 12 } 13 } catch (ClassNotFoundException e) { 14 // If still not found, then invoke findClass in order 15 // to find the class. 16 c = findClass(name); 17 } 18 } 19 if (resolve) { 20 resolveClass(c); 21 } 22 return c; 23 }
方法原理很简单,一步一步解释一下:
1、第5行,首先查找.class是否被加载过
2、第6行~第12行,如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就执行父加载器的loadClass方法,把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止
3、第13行~第17行,父加载器开始尝试加载.class文件,加载成功就返回一个java.lang.Class,加载不成功就抛出一个ClassNotFoundException,给子加载器去加载
4、第19行~第21行,如果要解析这个.class文件的话,就解析一下,解析的作用类加载的文章里面也写了,主要就是将符号引用替换为直接引用的过程
我们看一下findClass这个方法:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
是的,没有具体实现,只抛了一个异常,而且是protected的,这充分证明了:这个方法就是给开发者重写用的。
自定义类加载器
从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
2、如果想打破双亲委派模型,那么就重写整个loadClass方法
当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。
第一步,自定义一个实体类Person.java,我把它编译后的Person.class放在D盘根目录下:
1 package com.xrq.classloader; 2 3 public class Person 4 { 5 private String name; 6 7 public Person() 8 { 9 10 } 11 12 public Person(String name) 13 { 14 this.name = name; 15 } 16 17 public String getName() 18 { 19 return name; 20 } 21 22 public void setName(String name) 23 { 24 this.name = name; 25 } 26 27 public String toString() 28 { 29 return "I am a person, my name is " + name; 30 } 31 }
第二步,自定义一个类加载器,里面主要是一些IO和NIO的内容,另外注意一下defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class----只要二进制字节流的内容符合Class文件规范。我们自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只实现findClass方法:
public class MyClassLoader extends ClassLoader { public MyClassLoader() { } public MyClassLoader(ClassLoader parent) { super(parent); } protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { File file = new File("D:/Person.class"); return file; } private byte[] getClassBytes(File file) throws Exception { // 这里要读入.class的字节,因此要使用字节流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } }
第三步,Class.forName有一个三个参数的重载方法,可以指定类加载器,平时我们使用的Class.forName("XX.XX.XXX")都是使用的系统类加载器Application ClassLoader。写一个测试类:
1 public class TestMyClassLoader 2 { 3 public static void main(String[] args) throws Exception 4 { 5 MyClassLoader mcl = new MyClassLoader(); 6 Class<?> c1 = Class.forName("com.xrq.classloader.Person", true, mcl); 7 Object obj = c1.newInstance(); 8 System.out.println(obj); 9 System.out.println(obj.getClass().getClassLoader()); 10 } 11 }
看一下运行结果:
I am a person, my name is null com.xrq.classloader.MyClassLoader@5d888759
个人的经验来看,最容易出问题的点是第二行的打印出来的是"sun.misc.Launcher$AppClassLoader"。造成这个问题的关键在于MyEclipse是自动编译的,Person.java这个类在ctrl+S保存之后或者在Person.java文件不编辑若干秒后,MyEclipse会帮我们用户自动编译Person.java,并生成到CLASSPATH也就是bin目录下。在CLASSPATH下有Person.class,那么自然是由Application ClassLoader来加载这个.class文件了。解决这个问题有两个办法:
1、删除CLASSPATH下的Person.class,CLASSPATH下没有Person.class,Application ClassLoader就把这个.class文件交给下一级用户自定义ClassLoader去加载了
2、TestMyClassLoader类的第5行这么写"MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());", 即把自定义ClassLoader的父加载器设置为Extension ClassLoader,这样父加载器加载不到Person.class,就交由子加载器MyClassLoader来加载了
ClassLoader.getResourceAsStream(String name)方法作用
ClassLoader中的getResourceAsStream(String name)其实是一个挺常见的方法,所以要写一下。这个方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称是以"/"分隔的标识资源名称的路径名称。
不仅ClassLoader中有getResourceAsStream(String name)方法,Class下也有getResourceAsStream(String name)方法,它们两个方法的区别在于:
1、Class的getResourceAsStream(String name)方法,参数不以"/"开头则默认从此类对应的.class文件所在的packge下取资源,以"/"开头则从CLASSPATH下获取
2、ClassLoader的getResourceAsStream(String name)方法,默认就是从CLASSPATH下获取资源,参数不可以以"/"开头
其实,Class的getResourceAsStream(String name)方法,只是将传入的name进行解析一下而已,最终调用的还是ClassLoader的getResourceAsStream(String name),看一下Class的getResourceAsStrea(String name)的源代码:
1 public InputStream getResourceAsStream(String name) { 2 name = resolveName(name); 3 ClassLoader cl = getClassLoader0(); 4 if (cl==null) { 5 // A system class. 6 return ClassLoader.getSystemResourceAsStream(name); 7 } 8 return cl.getResourceAsStream(name); 9 } 10 11 private String resolveName(String name) { 12 if (name == null) { 13 return name; 14 } 15 if (!name.startsWith("/")) { 16 Class c = this; 17 while (c.isArray()) { 18 c = c.getComponentType(); 19 } 20 String baseName = c.getName(); 21 int index = baseName.lastIndexOf('.'); 22 if (index != -1) { 23 name = baseName.substring(0, index).replace('.', '/') 24 +"/"+name; 25 } 26 } else { 27 name = name.substring(1); 28 } 29 return name; 30 }
代码不难,应该很好理解,就不解释了。
.class和getClass()的区别
最后讲解一个内容,.class方法和getClass()的区别,这两个比较像,我自己没对这两个东西总结前,也常弄混。它们二者都可以获取一个唯一的java.lang.Class对象,但是区别在于:
1、.class用于类名,getClass()是一个final native的方法,因此用于类实例
2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象