双亲委派机制
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,类加载使用C++语言实现的,是虚拟机自身的一部分,负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
扩展(Extension)类加载器
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
//ExtClassLoader类中获取路径的代码 private static File[] getExtDirs() { //加载<JAVA_HOME>/lib/ext目录中的类库 String s = System.getProperty("java.ext.dirs"); File[] dirs; if (s != null) { StringTokenizer st = new StringTokenizer(s, File.pathSeparator); int count = st.countTokens(); dirs = new File[count]; for (int i = 0; i < count; i++) { dirs[i] = new File(st.nextToken()); } } else { dirs = new File[0]; } return dirs; }
系统(System)类加载器
应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
Java虚拟机对class文件采用的是按需加载的方式,需要使用该类时才会将它的class文件加载到内存生成class对象,加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理。
双亲委派模式
双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

如果一个类加载器收到了类加载请求,并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式 。
双亲委派模式优势
双亲委派模式的好处是Java类随着类加载器一起具备一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换

看出顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
loadClass(String)
方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先从缓存查找该class对象,找到就不用重新加载 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 thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // 如果都没有找到,则通过自定义实现的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; } }
findClass(String)
在JDK1.2之后建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的
defineClass(byte[] b, int off, int len)
defineClass()是用来将byte字节流解析成JVM能够识别的Class对象,通过方法不仅能够通过class文件实例化class对象,可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
自定义类加载器,父类加载器肯定为AppClassLoader
//自定义ClassLoader,完整代码稍后分析 class FileClassLoader extends ClassLoader{ private String rootDir; public FileClassLoader(String rootDir) { this.rootDir = rootDir; } // 编写获取类的字节码并创建class对象的逻辑 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //...省略逻辑代码 } //编写读取字节流的方法 private byte[] getClassData(String className) { // 读取类文件的字节 //省略代码.... } } public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { FileClassLoader loader1 = new FileClassLoader(rootDir); System.out.println("自定义类加载器的父加载器: "+loader1.getParent()); System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader()); System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent()); System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent()); /** 输出结果: 自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@29453f44 系统默认的AppClassLoader: sun.misc.Launcher$AppClassLoader@29453f44 AppClassLoader的父类加载器: sun.misc.Launcher$ExtClassLoader@6f94fa3e ExtClassLoader的父类加载器: null */ } }
编写自己的类加载器
实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。
-
当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
-
当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
-
当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。
自定义File类加载器
继承ClassLoader实现自定义的特定路径下的文件类加载器并加载编译后DemoObj.class,源码代码如下
public class DemoObj { @Override public String toString() { return "I am DemoObj"; } } public class FileClassLoader extends ClassLoader { private String rootDir; public FileClassLoader(String rootDir) { this.rootDir = rootDir; } /** * 编写findClass方法的逻辑 * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name, classData, 0, classData.length); } } /** * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return */ 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; } /** * 类文件的完全路径 * @param className * @return */ private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } public static void main(String[] args) throws ClassNotFoundException { String rootDir="/Users/Java8_Action/src/main/java/"; //创建自定义文件类加载器 FileClassLoader loader = new FileClassLoader(rootDir); try { //加载指定的class文件 Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } } 通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。如果继承URLClassLoader实现,那代码就更简洁了,如下: public class FileUrlClassLoader extends URLClassLoader { public FileUrlClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } public FileUrlClassLoader(URL[] urls) { super(urls); } public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(urls, parent, factory); } public static void main(String[] args) throws ClassNotFoundException, MalformedURLException { String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/"; //创建自定义文件类加载器 File file = new File(rootDir); //File to URI URI uri=file.toURI(); URL[] urls={uri.toURL()}; FileUrlClassLoader loader = new FileUrlClassLoader(urls); try { //加载指定的class文件 Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } }
自定义网络类加载器
自定义网络类加载器,主要用于读取通过网络传递的class文件(在这里我们省略class文件的解密过程),并将其转换成字节流生成对应的class对象,如下
public class NetClassLoader extends ClassLoader { private String url;//class文件的URL public NetClassLoader(String url) { this.url = url; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassDataFromNet(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } /** * 从网络获取class文件 * @param className * @return */ private byte[] getClassDataFromNet(String className) { String path = classNameToPath(className); try { URL url = new URL(path); InputStream ins = url.openStream(); 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 (Exception e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { // 得到类文件的URL return url + "/" + className.replace('.', '/') + ".class"; } }
获取字节码流时的区别,从网络直接获取到字节流再转成字节数组然后利用defineClass方法创建class对象,如果继承URLClassLoader类则和前面文件路径的实现是类似的,无需担心路径是filePath还是Url,因为URLClassLoader内的URLClassPath对象会根据传递过来的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认类Loader去读取对于的路径或者url下的class文件
双亲委派模型的破坏者-线程上下文类加载器
服务提供者接口(Service Provider Interface,SPI)允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行如下代码
//DriverManager是Java核心包rt.jar的类 public class DriverManager { //省略不必要的代码 static { loadInitialDrivers();//执行该方法 println("JDBC DriverManager initialized"); } //loadInitialDrivers方法 private static void loadInitialDrivers() { sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //加载外部的Driver的实现类 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //省略不必要的代码...... } }); }
DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,如下所示

ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,load()方法实现
public static <S> ServiceLoader<S> load(Class<S> service) { //通过线程上下文类加载器加载 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
确实通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
浙公网安备 33010602011771号