JVM专栏-类加载器和双亲委派机制
前言:在面试中,我们常被问及
JVM调优经验
、JVM内存区域知识
以及常用的JVM调优命令
。对于资深开发者而言,对JVM的不熟悉可能会影响高薪工作的获取。此外,JVM知识对于排查生产环境中的死锁
、内存溢出
、内存泄漏
等问题至关重要。本系列旨在从基础到深入,逐步加深对JVM的理解。相信坚持和收获总是成正比的,只愿今天的我比昨天的我更加努力一点,坚持的更久一点。
本篇是JVM专栏的第二篇,主要讲解以下内容:
- 类加载器的类型
- 双亲委派机制
- 如何打破双亲委派机制
- 自定义类加载器以及实际应用
1.类加载器
类加载器概述
当我们编写好的Java文件编译打包会生成一个Jar包或者War包,而类加载器负责将Jar包或者War包中的class文件加载到JVM虚拟机中,当然JVM把类加载到内存中然后再到方法调用是需要经过很多步骤的,我们一步一步去了解一下Class文件背后运行的原理。
在JAVA中,类加载器有四大分类:
-
引导类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar
、charsets.jar
等 -
扩展类加载器:
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的Jar类包 -
应用类加载器:
负责加载ClassPath路径下的类包,主要就是加载我们自己写的那些类 -
自定义加载器:
负责加载用户自定义路径下的类
写个Demo打印出每个类的类加载器
public class TestJdkClassLoader {public static void main(String[] args) {/*String 位于jre的lib下*/System.out.println(String.class.getClassLoader());/*DESKeyFactory 位于jre的lib下的ext目录*/System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());/*classPath路径下*/System.out.println(TestJdkClassLoader.class.getClassLoader().getClass().getName());System.out.println();//获取应用程序类加载器ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();//扩展类加载器ClassLoader extClassloader = appClassLoader.getParent();//获取引来类加载器ClassLoader bootstrapLoader = extClassloader.getParent();System.out.println("the bootstrapLoader : " + bootstrapLoader);System.out.println("the extClassloader : " + extClassloader);System.out.println("the appClassLoader : " + appClassLoader);System.out.println();System.out.println("bootstrapLoader加载以下文件:");URL[] urls = Launcher.getBootstrapClassPath().getURLs();for (int i = 0; i < urls.length; i++) {System.out.println(urls[i]);}System.out.println();System.out.println("extClassloader加载以下文件:");System.out.println(System.getProperty("java.ext.dirs"));System.out.println();System.out.println("appClassLoader加载以下文件:");System.out.println(System.getProperty("java.class.path"));}
}
运行结果:
null
sun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoaderthe bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@4b67cf4dthe appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2bootstrapLoader加载以下文件:
file:/D:/environment/jdk1.8/jre/lib/resources.jar
file:/D:/environment/jdk1.8/jre/lib/rt.jar
file:/D:/environment/jdk1.8/jre/lib/sunrsasign.jar
file:/D:/environment/jdk1.8/jre/lib/jsse.jar
file:/D:/environment/jdk1.8/jre/lib/jce.jar
file:/D:/environment/jdk1.8/jre/lib/charsets.jar
file:/D:/environment/jdk1.8/jre/lib/jfr.jar
file:/D:/environment/jdk1.8/jre/classesextClassloader加载以下文件:
D:\environment\jdk1.8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\extappClassLoader加载以下文件:
D:\environment\jdk1.8\jre\lib\charsets.jar;D:\environment\jdk1.8\jre\lib\deploy.jar;D:\environment\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\environment\jdk1.8\jre\lib\ext\cldrdata.jar;D:\environment\jdk1.8\jre\lib\ext\dnsns.jar;D:\environment\jdk1.8\jre\lib\ext\jaccess.jar;D:\environment\jdk1.8\jre\lib\ext\jfxrt.jar;D:\environment\jdk1.8\jre\lib\ext\localedata.jar;D:\environment\jdk1.8\jre\lib\ext\nashorn.jar;D:\environment\jdk1.8\jre\lib\ext\sunec.jar;D:\environment\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\environment\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\environment\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\environment\jdk1.8\jre\lib\ext\zipfs.jar;D:\environment\jdk1.8\jre\lib\javaws.jar;D:\environment\jdk1.8\jre\lib\jce.jar;D:\environment\jdk1.8\jre\lib\jfr.jar;D:\environment\jdk1.8\jre\lib\jfxswt.jar;D:\environment\jdk1.8\jre\lib\jsse.jar;D:\environment\jdk1.8\jre\lib\management-agent.jar;D:\environment\jdk1.8\jre\lib\plugin.jar;D:\environment\jdk1.8\jre\lib\resources.jar;D:\environment\jdk1.8\jre\lib\rt.jar;D:\devTools\idea\workspace\jvm_study\target\classes;D:\devTools\idea\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar
注意:
1.BootstrapLoader
是由c++语言实现的,所以会打印为null
2.虽然AppClassLoader
打印了jre/lib下的核心类库,但是它其实只加载class目录下的class类
类加载器的创建
我们知道程序运行的时候类加载器会加载我们编译的class文件,但是类加载器本身是由谁创建的呢?接下来我们跟随源码来一探究竟吧。
这里需要回顾下第一篇类加载子系统篇章的类是如何运行的。
当我们Java程序运行的时候,会创建一个引导类加载器(BootstrapLoader)
,再由这个引导类加载器
创建JVM启动器实例sun.misc.Launcher
,在Launcher类
构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)
和sun.misc.Launcher.AppClassLoader(应用程序类加载器)
。JVM默认使用Launcher
的getClassLoader()
方法返回的类加载器AppClassLoader
的实例加载我们的应用程序。
public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();//静态new出来private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}//构造方法中创建类加载器public Launcher() {Launcher.ExtClassLoader var1;try {//创建扩展类加载器var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {//创建应用类加载器,注意这里把应用类加载赋值给loader属性,并将扩展类加载器作为参数传递给了//getAppClassLoader方法this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}//设置线程上下文类加载器Thread.currentThread().setContextClassLoader(this.loader);}
}
在Launcher.ExtClassLoader.getExtClassLoader()
中创建扩展类加载器,这里会调用到顶层ClassLoader类的构造方法,只不过这里扩展类加载器调用父类构造方法时传的
parent`为null
public ExtClassLoader(File[] var1) throws IOException {super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}
在Launcher.AppClassLoader.getAppClassLoader(var1)
中创建应用类加载器,这里会把ExtClassLoader
作为参数传入进来,注意,这里的两个类加载器不是类上的继承关系,只是AppClassLoader
的parent
属性指向ExtClassLoader
实例
AppClassLoader(URL[] var1, ClassLoader var2) {super(var1, var2, Launcher.factory);this.ucp.initLookupCache(this);}
顶层父类-ClassLoader
private ClassLoader(Void unused, ClassLoader parent) {this.parent = parent;}
这里只有AppClassLoader
的parent
属性指向了ExtClassLoader
,而ExtClassLoader
并没有指向BootstrapLoader
,因为BootstrapLoader
是由C++编写的,我们JDK中是无法看到的,但是这里不会影响ExtClassLoader
委托BootstrapLoader
去加载类,这块会在双亲委派机制介绍parent属性的左右。
2.双亲委派机制
什么是双亲委派机制
当类加载器加载某个类时,会先检查自己加载过的类中是否存在,如果不存在会先委托父加载器寻找目标类,如果还是找不到则继续再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并加载目标类。
-
检查顺序是自底向上:加载过程中会先检查类是否被已加载,从
Custom ClassLoader
到BootStrapClassLoader
逐层检查,只要某个Classloader
已加载就视为已加载此类,保证此类只会被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) {//parent属性不为空则调用父加载器加载类c = parent.loadClass(name, false);} else {//如果parent为空,则调用引导类加载器加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// 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();//调用findClass方法加载指定名称的类c = findClass(name);}}return c;}}
走读核心代码逻辑:
findLoadedClass:
判断是否已经加载过此类,如果没有加载过走一下逻辑parent
不为空,则先由父类加载器加载,为空,则由BootstrapClassLoader
去加载(这也说明了为什么ExtAppClassLoader
的Parent
属性为空,也可先由BootstrapClassLoader
去加载)- 如果父类加载器加载不到,最后由自己调用
findClass
方法加载,需要注意的是findClass
是一个抽象方法,由子类实现。
双亲委派的优点
双亲委派机制的优点:
- 沙箱安全机制:自己写的
java.lang.String.class
类不会被加载,这样便可以防止核心API库
被随意篡改 - 避免类的重复加载:当父亲已经加载了该类时,就没有必要
子ClassLoader
再加载一次,保证被加载类的唯一性
我们可以自己试试自定义一个String
类,看看是否可以正常被加载
//包名也一样
package java.lang;
public class String {public static void main(String[] args) {System.out.println("******自定义String的Main方法");}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application这是因为双亲委派机制的存在,当我们要加载java.lang.String的时候,应用程序类加载会向上委托,而我们的jre的lib下也有一个相同类路径的String类,此时会返回这个String类信息,但是这个String类是没有main方法的,就会出现以上错误。
全盘负责委托机制
“全盘委托”
是指当一个ClassLoder
装载一个类时,除非显示的使用另外一个ClassLoder
,该类所依赖及引用的类也由这个ClassLoder
载入(已经被加载过的类除外)。
3.创建自定义类加载器
自定义类加载器只需要继承 java.lang.ClassLoader
类,该类有两个核心方法,一个是loadClass(String, boolean)
,实现了双亲委派机制,还有一个方法是findClass
,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
自定义类加载器
public class MyClassLoader extends ClassLoader {private String classPath;//加载路径public MyClassLoader(String classPath) {this.classPath = classPath;}//把class文件加载成字节流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;}@Overrideprotected 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();}}
创建一个测试类,作为我们外部类需要加载到项目中
package com.lx;
public class People {public void say(){System.out.println("加载成功");}
}
编译后把原项目的class文件放在D盘的/test/com/lx目录下,
测试
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InvocationTargetException {//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为 //应用程序类加载器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("D:/test");//D盘创建 test/com/lv 目录,将People.class丢入该目录Class clazz = classLoader.loadClass("com.lx.People");Object obj = clazz.newInstance();Method method = clazz.getDeclaredMethod("printf", null);method.invoke(obj, null);System.out.println(clazz.getClassLoader().getClass().getName());}
运行结果:
加载成功
org.bx.idgenerator.MyClassLoaderTest$MyClassLoader
4.双亲委派机制的打破
为什么需要打破双亲委派机制
在某些情况下,父类加载器需要加载的class文件
受到加载范围的限制,无法加载到需要的文件,这个时候就需要委托子类加载器进行加载。这种情况就打破了双亲委派模式。
举个例子:
以DriverManager为例,DriverManager定义在JDK中,其内部的数据库驱动实现由各个数据库的服务商来提供,如MySQL
、Oracle
、SQLServer
等等,都实现了该接口驱动接口,这些实现类都是以jar包的形式放到classpath目录下。那么问题来了:DriverManager基于SPI机制加载各个实现了Driver接口的实现类(在classpath下)进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现类是由服务商提供的,由应用类加载器加载。这个时候,就需要扩展类加载器来委托子类来加载Driver实现,这就破坏了双亲委派。类似情况还有很多,比如Tomcat如何隔离不同应用所依赖jar包,Jrebel的热部署机制等等。
DriverManager使用SPI机制
打破双亲委派机制
private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader// exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});....}
源码走读:
ServiceLoader.load
方法会加载我们META-INF/services/下文件指定的Driver接口的实现类
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}
在这里重点看ClassLoader cl = Thread.currentThread().getContextClassLoader();
是从当前线程中拿到了一个上下文类加载器,这个类加载其实是在我们程序启动的时候会把AppClassLoader类加载放在线程的上下文中,参看Launcher类的构造方法
如何打破双亲委派机制
在我们自定义的类加载器中,其实只需要重新父类ClassLoader的loadClass方法
/*** 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载* @param name* @param resolve* @return* @throws ClassNotFoundException*/@Overrideprotected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> 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 statssun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}if (resolve) {resolveClass(c);}return c;}}}
我们重新把Person类放在工厂的类文件目录下,然后运行代码:
java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.(FileInputStream.java:138)
at java.io.FileInputStream.(FileInputStream.java:93)
at com.lx.MyClassLoaderTest
结果提示的找不到Object类,这是因为我们所有的类都继承于Object类,而自定义类加载器加载People类的时候找不到Object类,所以就会出现这个错误,这里我们可以怎么解决呢?我们需要修改下代码,自定义类加载器只加载自己想加载的类,而基础的类还遵循双亲委派机制
@Overrideprotected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();//自定义包下的类由自定义类加载器加载if(name.startsWith("com.lx")){c = findClass(name);}else {c = this.getParent().loadClass(name);}// this is the defining class loader; record the statssun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}if (resolve) {resolveClass(c);}return c;}}}
运行结果:
加载成功
org.bx.idgenerator.MyClassLoaderTest$MyClassLoader
通过深入理解类加载器和双亲委派机制,我们可以更好地掌握JVM的工作原理,这对于Java开发人员来说是一个不可或缺的技能。希望本文能够帮助你更深入地理解这些概念,并在实际开发中运用自如。