1.类加载过程
多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载到JVM。主类在运行过程中如果使用到其他类,会逐步加载这些类。也就是说,jar包中的类并不是一次性全部加载的,是使用到时候才进行加载。
类加载到使用整个过程有如下几步:
加载$\Rightarrow$验证$\Rightarrow$准备$\Rightarrow$解析$\Rightarrow$初始化$\Rightarrow$使用$\Rightarrow$卸载
这里对每一个步骤进行说明
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类的时候才会加载,例如调用类的main()方法,新建对象等。
- 验证:校验字节码文件的正确性。字节码文件的头几位会判断Java版本,当java8生成的字节码用java6启动的时候可能会导致报错。
- 准备:将类的静态变量分配内存,赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法替换为指向数据所存内存的指针或句柄等(直接引用)),这就是所谓的静态链接过程(类加载期间完成),动态链接是程序运行期间完成的,将符号引用替换为直接引用
- 初始化:对类的静态变量赋予指定值,执行静态代码块

2. 类加载器和双亲委派机制
上面类的加载过程主要是通过类加载器来实现的,在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。
所以你可以从ZIP包中读取,从网络中获取,运行时计算生成(动态代理),从其他文件生成(.jsp本质是class)
Java里有如下几种类加载器
- 启动类加载器:负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 扩展类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
- 应用程序类加载器:负责加载classpath路径下的类包。主要就是加载你自己写的类
- 自定义加载器:负责加载用户自定义路径下的类包
启动类加载器属于虚拟机的一部分,它是用C++写的,看不到源码,所以如果试图通过
System.out.println(String.class.getClassLoader());
打印的是null值
2.1 自定义一个类加载示例
自定义类加载器只需要继承java.lang.ClasslLoader类,该类有两个核心方法,一个是loadClass(String,boolean),实现了双亲委派机制,大体逻辑是
- 首先,检查一下指定名称的类是否已经加载过,如果加载过,就不需要再加载,直接返回
- 如果此类没有加载过,那么判断是否有父类加载器。如果有父类加载器,便通过父类加载器加载(即调用parent.loadClass(name,false);),或者是调用bootstrap类加载器来加载
- 如果父加载器及bootstrap类加载器都没有找到指定类,那么调用当前类加载器的findClass方法来完成加载。
还有一个方法是findClass,默认实现是抛出异常,所以我们自定义类加载器主要是重写findClass方法
自定义类加载器示例:
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
=======自己的加载器加载类调用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader
2.2 双亲委派机制
有一个描述类加载器加载类过程的术语:双亲委派模型。然而这是一个很有误导性的术语,它应该叫做单亲委派模型(Parent-Delegation Model)。但是没有办法,大家都已经这样叫了。所谓双亲委派,这个亲就是指ClassLoader里的全局变量parent,也就是父加载器。

双亲委派的具体过程如下:
- 当一个类加载器接收到类加载任务时,先查缓存中有没有,如果没有,将任务委托给它的父类加载器去执行
- 父类加载器也做统一的过程,一层一层网上委托,直到最顶层的启动类加载器为止。
- 如果启动类加载器没有找到所需加载的类,便将此加载任务退回给下一级类加载器去执行,而下一级的类加载器也做同样的事情
- 如果最底层类加载器仍然没有找到所需要的class文件,则抛出异常
所以是一条线传上再传下,并没有什么“双亲”。整个过程的Java也没有什么神秘的:
public abstract class ClassLoader {
// name: Class文件的绝对路径
// resolve: 找到后是否立即解析(什么是解析?)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (lock) {
// 尝试从缓存获取,这也是为什么修改了Class后需重启JVM才能生效
Class<?> target = findLoadedClass(name); // native方法
if (target == null) {
try {
if (parent != null) {
// 委托给父加载器, 只查找不解析
target = parent.loadClass(name, false);
} else {
// 父加载器为null,则委托给启动类加载器BootstrapClassloader
target = findBootstrapClassOrNull(name); // native方法
}
} catch (ClassNotFoundException e) {...}
if (target == null) {
// 父加载器没有找到,才调用自己的findClass()方法
target = findClass(name);
}
}
if (resolve) {
resolveClass(target); // native方法
}
return target;
}
}
// findClass是模板方法,需要重写
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
2.3 为什么要设计双亲委派机制
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样可以防止核心API库被篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。
如果你打算自己写一个String类,然后在其中写main()方法,这是可以做到的,但是运行的时候会抛出异常:在类java.lang.String中找不到main()方法。
如果你说想打破双亲委派机制?
沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
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.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
//尝试用自己改写类加载机制去加载自己写的java.lang.String.class
Class clazz = classLoader.loadClass("java.lang.String");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
2.4 打破双亲委派机制
以tomcat为例,在tomcat中就实现了打破双亲委派机制,为什么需要这样做?
首先,tomcat是一个web容器,需要解决的问题有:
- 一个web容器可能需要部署多个应用程序,那么不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离
- 部署在同一个web容器中的相同类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么会有10份相同的类库加载进虚拟机
- web容器也有自己的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持热部署。比如jsp文件最终也是要编译成class文件才能在虚拟机中运行,但是程序运行后修改jsp是司空见惯的事,web容器需要支持jsp修改后不用重启。
如果使用双亲委派机制,那么意味着无法加载两个相同类库的不同版本,默认的类加载器不管你是什么版本,只在乎你的全限定名,并且只有一份。
如果要实现jsp文件的热加载,对某个jsp进行修改,但是类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。所以要实现,我们需要卸载这jsp文件的类加载器,然后重新创建类加载器,重新加载jsp文件。
2.5 tomcat自定义类加载器

tomcat中的几个主要类加载器
- commonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身以及各个webapp访问
- catalinaLoader:tomcat容器私有的类加载器,加载路径中的class对于webapp不可见
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有的webapp可见,但是对于tomcat容器不可见
- webappClassLoader:各个webapp私有的类加载器,加载路径中的class只对当前webapp可见
从图中的委派关系可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat通过打破Java的双亲委派机制,实现每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。