我们已经知道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文件,只要类加载器不相同,那么这两个类对象注定不会相等。