一、概述
以.java为扩展名的文件经过编译会生成对应的.class文件,需要执行的时候,虚拟机首先需要从class文件中读取必要的信息,而这个过程则称为类加载。类加载是类的生命周期的一部分,也是类的初始步骤。类加载器可以完成将类加载到虚拟机的工作。加载器加载类的时候是通过该类的全限定名来获取描述此类的二进制字节流。
在加载阶段,虚拟机需要完成3件事情:
a) 通过一个类的全限定名来获取定义此类的二进制字节流;
b) 将这个字节流所代表的静态存储结构 转化为方法区的运行时数据结构;
c) 在内存中生成一个代表这个类的java.lang.Class类对象,作为方法区中这个类的访问入口。
类加载器会去获取类的信息,但没有规定类的格式。具体的格式通常有一下几种:
文件格式: .class文件、.jar文件、.war文件等;
动态生成:程序在执行过程中,动态生成类的字节码,主要表现为动态代理;
其他文件生成:比如说由JSP文件生成对应的.class文件,网络获取,Applet。
加载阶段完成后,虚拟机外部的二进制字节流 就按照虚拟机所需的格式存储在方法区 之中。然后在内存中实例化一个java.lang.Class类的对象(作为程序访问方法区中的这些类[型]数据的外部接口)。
二、设计原理
虚拟机团队在设计类加载过程时,通过一个类的全限定名来获取此类的二进制字节流,这个操作放到了虚拟机外部去实现,目的是让应用程序自己决定如何去获取所需要的类。而实现这个加载流程的代码就是类加载器。
对于任意一个类,都需要由它的类加载器和这个类本身 一同确立其在JVM中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。就是说在代码里面import某个类时,虚拟机去查找这个类,需要类加载器 和全限定名 才能唯一确定一个类。这样做其实是很容易理解的。举个例子:如果有A、B两个应用程序同时部署在Tomcat上,而且A、B上有命名相同的包,假设com.wpr,里面还存在同名类Clazz,对于B应用import com.wpr.Clazz对象,如果仅仅是限定名称,那么Tomcat将无法区别这两个类。在这种情况下,需要不同的类加载器去加载各自的com.wpr.Clazz类,通过类加载器+全限定名才能唯一确定需要的类。所以比较两个类是否相同,只有在这两个类是在同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。
虚拟机自带的类加载器,不同的类需要不同的类加载器去加载,再看一下虚拟机自带的类加载器。
从Java虚拟机的角度来讲,只存在两种不同的类加载器:
a) 启动类加载器(Bootstrap ClassLoader),这个类加载器是用C++语言实现的,是虚拟机的一部分。
b) 其他的类加载器,这些类加载器都是有Java语言实现,独立于虚拟机且全部继承自抽象类java.lang.ClassLoader。
从Java开发者角度看,将类加载器可以区分为三种:
a) 启动类加载器(Bootstrap ClassLoader)
这个类负责加载存放在/lib目录下,或者-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类(引导类)加载器,可以直接使用null代替。
b) 扩展类加载器(Extension ClassLoader)
这个加载器由sum.misc.Launcher$ExtClassLoader实现,它负责加载/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的素有类库,开发者可以直接使用扩展类加载器。
c) 应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Laucher$AppClassLoader实现,称为系统类加载器,负责加载用户类路径(ClassPath)上指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下使用的就是这个默认的类加载器。
存在这么多类加载器,一个类应当由谁来加载?,这个就涉及到了Java的类加载机制---双亲委派模型。

双亲委派模型的目的就是确定由那个类加载器来加载类。
三、编写自己的ClassLoader
继承ClassLoader后,由于loadClass方法是双亲认证的流程,因此,只有父类找不到类型信息,才需要自定义的类加载器来加载,因此无需覆盖原来的loadClass方法。在编写自己的类加载器时,只需要重新编写findClass方法即可。
java
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);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
loadClass方法抛出的是 java.lang.ClassNotFoundException异常;
defineClass方法抛出的是 java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的该类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,全限定名相同的类只加载一次,即 loadClass方法不会被重复调用。