我们已经知道Class类是描述类的信息的类,在我们使用一个类之前,JVM会将该类的字节码文件(.Class)从磁盘,网络或者其他的来源加载到内存中,并对字节码进行解析,生成Class对象。在Class类中有提供forName()方法,此方法根据ClassPath所配置的路径进行类的加载,如果你的类来源是网络,文件。那么这个时候我们就要手动实现类加载器。
首先我们要了解类加载器是什么:在JVM的类加载阶段中有一个动作叫做“通过一个类的全限定名来描述此类的二进制字节流”。这个动作被放在JVM的外部实现,以便让应用程序自己决定去如何获取所需要的类。实现这个动作的代码块就叫做“类加载器”。
现在我们来介绍一下ClassLoader。首先用一副图来介绍一下它。
这张图中有四个要介绍的加载器,我现在来逐一介绍
1:Bootstrap(启动类加载器):只有这个类加载器是在JVM的内部的,这个加载器使用C++实现。除了这个类加载器以外,其他的类加载器都是Java实现的并且存在于JVM的外部,并且都是java.lang.ClassLoader类的子类。这个加载器的作用是加载<Java_Runtime_Home>/lib目录中的文件,并且只加载特定文件名的文件,就是说如果你才这个目录下放了一个别的.jar文件,此加载器都是不会加载它的。除此之外,因为这个加载器是JVM的一部分,所以该加载器无法被Java程序使用。
2:ExtClassLoader(扩展类加载器):负责加载<Java_Runtime_Home>/lib/ext目录下或者被java.ext.dirs系统变量指定路径下的类库,此加载器可以被开发者直接使用。
3:AppClassLoader(应用程序类加载器):负责加载用户类路径中的文件,如果用户没有自定义类加载器,那么此加载器就会是程序中的默认类加载器。
类加载器中的双亲委派模型
双亲委派模型可以保证Java程序的稳定执行,首先看一下类加载器之间的关系,用一幅图来表示。
关于类的加载与双亲委派模型,有四点需要我们总结
1:类的加载过程由代理设计模式实现
2:这四种加载器的层次关系就叫做双亲委派模型
3:除了最顶层的Bootstrap加载器之外,其余的加载器都要有自己的父类,要注意的是,这里的父类加载器不是通过继承来实现的,而是采用组合的方式实现。
4:当一个加载器收到加载请求时,先不自己处理,而是把加载请求委托给父加载器处理,每一层的加载器都是如此。所以只有当BootStrap加载器都没有办法处理加载请求的时候,子加载器才会尝试自己去加载。这就是双亲委派模型的工作流程。例如java.lang.Object类,它存放在rt.jar中,就是说无论我们用哪一个加载器都会加载这个类。所以我们会得到一个结论:Object类在各种类加载器的环境里都是同一个类。
双亲委派模型从JDK1.2之后引入,但是不强制要求,可以破坏此机制来加载类。最典型的案例就是OSGI(Java模块化技术)也叫做热加载,大致的意思就是:在JVM进程运行的过程中,如果新的类加入,不用重启JVM也可以加载那个类。
ClassLoader进行实现双亲委派机制
首先我们要看一下ClassLoader类中的loadClass方法源码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查类是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果没有找到类,就抛出ClassNotFoundException异常 // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
首先ExtClassLoader和AppClassLoader都继承于ClassLoader类,根据loadClass方法的源码来总结一下,整个类的加载过程可以分成如下的几个步骤。
1:首先查看要加载的类是否已经被加载过了。
2:如果还没有被加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类加载器去加载,如果为空的话(Bootstrap加载器在Java程序中不可见,所以为null),就调用Bootstrap(启动类加载器)去加载。
3:如果在第二步加载失败了,就调用自定义加载器去进行加载。
自定义加载器
其实我们在大多数情况下都使用系统的加载器进行类的加载,但是在有些特定的情况下我们不得不使用自定义的类加载器:假设我们现在从网络上获取了一个类放在桌面上,这个时候系统类的加载器就没有办法对其进行加载。所以在这个时候我们就需要来自定义加载器,自定义加载器一般都是继承于ClassLoader类并且覆写findClass方法即可。以下我通过一个例子来说明自定义加载器的流程。
import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.InputStream; class MyClassLoader extends ClassLoader{//自定义加载器 public Class<?> LoadData(String classname)throws Exception{//输入要加载的类的名称 byte[] classdata = this.loadClassData(); return super.defineClass(classname,classdata,0,classdata.length); } private byte[] loadClassData() throws Exception{//通过指定的路径进行文件加载,也就是二进制文件读取 InputStream input = new FileInputStream("C:\\Users\\Lenovo\\Desktop\\student1.class");//桌面上的.class文件路径 //拿到所有字节内容,放到内存中 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); //读取输出缓存区 byte[] data = new byte[50]; int temp = 0; while((temp = input.read(data))!=-1){ byteArrayOutputStream.write(data,0,temp); } byte[] result = byteArrayOutputStream.toByteArray(); input.close(); byteArrayOutputStream.close(); return result; } } public class Main{ public static void main(String[] args)throws Exception{ Class<?> cls = new MyClassLoader().LoadData("student1"); System.out.println(cls.getClassLoader()); System.out.println(cls.getClassLoader().getParent()); System.out.println(cls.getClassLoader().getParent().getParent()); } }
我们会发现放在桌面上的.class文件被我自定义的类加载器所加载。说明了自定义类加载器可以对动态的类路径进行加载操作。此外,最好不要覆写loadClass方法,这样会破坏双亲委托模式。
最后一点:当比价两个类对象是否相等的时候,必须要有同一个类加载器加载的时候才有意义。否则,即使两个类来自于同一个.class文件,只要类加载器不相同,那么这两个类对象注定不会相等。