JVM类加载机制—类加载器和双亲委派机制详解
一、概述
上篇我们介绍了JVM类加载机制—JVM类加载过程,类加载过程是类加载机制第一阶段,这一阶段主要做将类的字节码(class文件)加载JVM内存模型中,并转换为JVM内部的数据结构(如java.lang.Class实例),便于执行。其中类加载器是JVM用于加载类文件的一个子系统,主要是通过类的全限定名来定位和加载类的二进制文件。
二、类加载器
1、类加载器分类
JVM支持两种类型的类加载器,分别为JDK自带类加载器和自定义类加载器。其中自带类加载器包括引导类加载器(启动类加载器/根类加载器)、扩展类加载器、应用程序类加载器。
引导类加载器(Bootstrap ClassLoader):也加启动类加载器或根类加载器。负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等。不继承java.lang.ClassLoader,是扩展类加载器的父加载器(不是父类)。底层有C++实现,在java代码中无法直接获取到引用,返回null。
扩展类加载器(Extension ClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包,是应用程序类加载器的父加载器。由java代码编写,继承ClassLoader类。
应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载ClassPath路径下的类包,主要就是加载自己写的那些类。由java代码编写,继承ClassLoader类,为程序中默认的类加载器。
自定义类加载器(User-Defined ClassLoader):负责加载用户自定义路径下的类包,支持一些个性化的功能。自定义类加载器只需继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。
自定义类加载器示例代码如下
import java.io.FileInputStream;
/**
* 自定义类加载器
*/
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
//根据类名从磁盘上将类文件 读取到byte[]数组中
private byte[] loadByte(String className) throws Exception {
className = className.replace("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + className + ".class");
int length = fis.available();
byte[] bytes = new byte[length];
fis.read(bytes);
fis.close();
return bytes;
}
//根据类名 加载Class类
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes= null;
try {
bytes = loadByte(name);
//defineClass方法将一个字节数组转换为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws Exception {
//初始化类自定义类加载器,会先初始化父类ClassLoader
MyClassLoader myClassLoader = new MyClassLoader("D:/test");
//D盘创建test/com/test/jvm/ 几级目录,将User1.class放入该目录
Class clazz = myClassLoader.loadClass("com.test.jvm.User1");
Object object = clazz.newInstance();
//通过反射获取类中sout方法
Method method = clazz.getDeclaredMethod("sout", new Class<?>[]{});
//执行该方法
method.invoke(object, null);
//打印这个类的类加载器 MyClassLoaderTest$MyClassLoader
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
通过下面示例可以测试下类所使用的具体类加载器
/**
* 类加载器过程
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
//核心程序类 支撑JVM运行 位于lib目录下 引导类加载器
System.out.println("引导类加载器:" +String.class.getClassLoader());
//扩展程序类 支撑JVM运行 位于lib目录中ext下的包 扩展类加载器
System.out.println("扩展类加载器:" +DESKeyFactory.class.getClassLoader());
//自己编写的程序 运行自身业务流程 应用程序类加载器
System.out.println("应用程序类加载器:" +TestJDKClassLoader.class.getClassLoader());
}
}
运行结果
通过上面结果我们可以看出 引导类加载器底层由于是C++实现,在java中无法获取,而扩展类加载器和应用程序类加载器 都是sun.misc.Launcher类下的内部类。
引导类加载器是扩展类加载器的父加载器,扩展类加载器是应用程序类加载器的父加载器,但三者之间没有继承或实现的关系。
2、类加载初始化过程
通过类加载全过程图(加载过程)知道,类加载会创建JVM的sun.misc.Launcher类实例的启动器。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。通过Launcher类的getLauncher()方法来获取类自己的加载器ClassLoader。
getLauncher()源码
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
//单例模式设计,保证一个JVM中只有一个Launcher实例
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
//获取Launcher类实例
public static Launcher getLauncher() {
return launcher;
}
//Launcher类构造方法
public Launcher() {
ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用程序类加载器,在构造过程中将其父加载器设置为ExtClassLoader
//Launcher类的loader属性只是AppClassLoader,一般都是用这个类加载器来加载我们写的代码
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
//。。。。。省略一部分不关注的代码
}
}
三、JVM类加载机制种类
全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
四、双亲委派机制
JVM为了避免类的重复加载,确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。JVM会采用双亲委派模型进行加载,双亲委派模型要求除了引导类加载器外,其余的类加载器都应当有自己的父加载器。
1、工作原理
当类加载器收到加载一个类的请求时,它首先不会尝试自己去加载这个类,而是为先委托父加载器寻找这个类,找不到就会在委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。简单来说先找父加载器加载,不行再由子加载器自己加载。加载流程如下:
我们来看下应用程序类加载器APPClassLoader加载类的双亲委派机制的源码。APPClassLoader加载器的loadClass方法最终会调用其父类ClassLoader的loadClass方法。该方法大体逻辑如下:
1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。
2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false));或者是调用bootstrap类加载器来加载。
3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
ClassLoader.loadClass()源码
//ClassLoader的loadClass方法,里面实现了双亲委派机制
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) {
long t0 = System.nanoTime();
try {
//如果当前类加载器的父加载器不为空,则委托父加载器加载该类
//AppClassLoader 父加载器为ExtClassLoader,直接执行还是到这个loadClass方法
ExtClassLoader加载器的父加载器为null(引导类加载器是C++实现,所以为null)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果当前加载器的父加载器为空,则委托引导类加载器加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//当该类通过类加载器加载为空时,通过findClass方法加载类
//ExtClassLoader无findClass方法,通过父类URLClassLoader.findClass方法加载类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//通过父类URLClassLoader.findClass方法加载类
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;
}
}
URLClassLoader.findClass方法源码
//类加载器通过findClass方法尝试加载类
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//通过类的name 获取类的路径
//如com.test.jvm.Math 获取path = com/text/jvm/Math.class
String path = name.replace('.', '/').concat(".class");
//通过类路径从磁盘上拿到类文件
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//通过defineClass方法将类装载到JVM中
//ExtClassLoader加载器是装载失败的,因为ExtClassLoader加载的是ext包下的路径,返回为null
//再会执行AppClassLoader加载器的defineClass进行加载,才会装载JVM成功
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
2、双亲委派机制意义
沙箱安全机制:确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。如自己写的java.lang.String.class类是不会被加载的
类的唯一性:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,避免重复加载,保证被加载类的唯一性
上一篇: JVM类加载机制—类加载过程