更多请移步: 我的博客

初识ClassLoader

  1. 在开发中有时会碰到ClassNotFoundException。这个异常和ClassLoader有着密切的关系。

  2. 我们常使用instanceof关键字判断某个对象是否属于指定Class创建的对象实例。如果对象和Class不属同一个加载器加载,那么instanceof返回的结果一定是false。

  3. GC Root有一种叫做System Class,官方解释“Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .”,大意是:被bootstrap/system加载器加载的类,比如,像java.util.*这些来自rt.jar的类。

  4. GC时对Class的卸载,需要满足的条件如下:

类需要满足以下3个条件才能算是“无用的类”
- 该类所有的实例已经被回收
- 加载该类的ClassLoder已经被回收
- 该类对应的java.lang.Class对象没有任何对方被引用

ClassLoader简介

我们的Java应用程序都是由一系列编译为class文件组成,JVM在运行的时候会根据需要(比如:我们需要创建一个新对象,但是该对象的Class定义并未在Perm区找到)将应用需要的class文件找到并加载到内存的指定区域供应用使用,完成class文件加载的任务就是由ClassLoader完成。

ClassLoader类的基本职责就是根据一个指定的类的名称,找到(class可能来源自本地或者网络)或者生成其对应的字节代码,然后从这些字节代码中定义出一个java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

在JVM中每个ClassLoader有各自的命名空间,不同的ClassLoader加载的相同class文件创建的Class实例被认为是不相等的,由不相等的Class创建的对象实例无法相互强制转型,如开头所提,当我们使用instanceof关键字判断时需要注意。

双亲委派模型

双亲委派模型很好理解,直接上代码。

/**
* 使用指定的二进制名称来加载类。默认的查找类的顺序如下:
* 调用findLoadedClass(String) 检查这个类是否被加载过;
* 调用父加载器的loadClass(String),如果父加载器为null,使用虚拟机内置的加载器代替;
* 如果父类未找到,调用findClass(String)方法查找类。
*/
protected 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) {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// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}

从源码中我们看到有三种类加载器:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码(C++)来实现的,并不继承自java.lang.ClassLoader。负责将${JAVA_HOME}/lib目录下和-Xbootclasspath参数所指定的路径中的,并且是Java虚拟机识别的(仅按照文件名识别,如rt.jar,不符合的类库即使放在lib下也不会被加载)类库加载到JVM内存中,引导类加载器无法被Java程序直接引用;

  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库(${JAVA_HOME}/ext),或者被java.ext.dirs系统变量所指定的路径中的所有类库;

  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

public class ClassLoaderTree {/*** 输出:* sun.misc.Launcher$AppClassLoader@18b4aac2* sun.misc.Launcher$ExtClassLoader@5305068a* null* @param args*/public static void main(String[] args) {ClassLoader loader = ClassLoaderTree.class.getClassLoader();while (loader!=null){System.out.println(loader.toString());loader = loader.getParent();}System.out.println(loader);}
}

每个Java类都维护着一个指向定义它的类加载器的引用,通过getClassLoader()方法就可以获取到此引用。通过调用getParent()方法可以得到加载器的父类,上述代码输出中,AppClassLoader对应系统类加载器(system class loader);ExtClassLoader对应扩展类加载器(extensions class loader);需要注意的是这里并没有输出引导类加载器,这是因为有些JDK的实现对于父类加载器是引导类加载器。这些加载器的父子关系通过组合实现。

为什么要双亲委派

  1. 避免重复加载。当父亲已经加载了该类,子类就没有必要再加载一次;
  2. 安全。如果不使用这种委托模式,那我们就可以使用自定义的String或者其他JDK中的类,存在非常大的安全隐患,而双亲委派使得自定义的ClassLoader永远也无法加载一个自己写的String。

创建自定义ClassLoader

自定义的类加载器只需要重写findClass(String name)方法即可。java.lang.ClassLoader封装了委派的逻辑,为了保证类加载器正常的委派逻辑,尽量不要重写findClass()方法。

public class FileSystemClassLoader extends ClassLoader {private String rootDir;public FileSystemClassLoader(String rootDir){this.rootDir = rootDir;}@Overrideprotected 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 = 1024;byte[] buffer = new byte[bufferSize];int bytesNumRead = 0;while ((bytesNumRead = ins.read(buffer)) != -1){baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}private String classNameToPath(String className) {return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";}
}

实验

更多实验代码放在github上:
https://github.com/Childe-Chen/goodGoodStudy/tree/master/src/main/java/com/cxd/classLoader

public class TestClassIdentity {public static void main(String[] args) {FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("/Users/childe/Documents/workspace/goodGoodStudy/target/classes");try {Class<?> c = fileSystemClassLoader.findClass("com.cxd.classLoader.Sample");//forName会执行类中的static块(初始化)Class<?> c1 = Class.forName("com.cxd.classLoader.Sample");System.out.println(c1.isAssignableFrom(c));//运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 o1 o2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。//不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。// 不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到Object o = c.newInstance();Method method = c.getMethod("setSample", java.lang.Object.class);Object o1 = c1.newInstance();method.invoke(o,o1);} catch (Exception e) {e.printStackTrace();}}
}
public class Sample {private Sample instance;static {System.out.println("static");}public void setSample(Object instance) {this.instance = (Sample) instance;}
}

打破双亲委派模型

没有完美的模型,双亲委派在面对SPI时,不得不做出了特例或者说改进。我们知道Java提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的有JDBC、JCE、JNDI、JAXP 和 JBI 等。SPI的接口由Java核心库定义,而其实现往往是作为Java应用所依赖的 jar包被包含到CLASSPATH里。而SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,由引导类加载器加载;SPI的实现类是由系统类加载器加载,引导类加载器无法找到SPI的实现类,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

为了解决这个问题,Java引入了线程上下文类加载器,在Thread中聚合了contextClassLoader,通过Thread.currentThread().getContextClassLoader()获得。原始线程的上下文ClassLoader通常设定为用于加载应用程序的类加载器。也就是说父加载器可以通过县城上下文类加载器可以获得第三方对SPI的实现类。

以Java链接Mysql为例,看下Java如何来加载SPI实现。

// 注册驱动,forName方法会初始化Driver,初始化块中向DriverManager注册驱动
Class.forName("com.mysql.jdbc.Driver").getInstance();
String url = "jdbc:mysql://host:port/db";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

com.mysql.jdbc.Driver是java.sql.Driver的一种实现。

package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager// 向DriverManager注册驱动static {try {java.sql.DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}...
}

接下来我们调用getConnection就进入了本小结的关键点。

//  Worker method called by the public getConnection() methods.private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {/** 再次强调下:原始线程的上下文ClassLoader通常设定为用于加载应用程序的类加载器* When callerCl is null, we should check the application's* (which is invoking this class indirectly)* classloader, so that the JDBC driver class outside rt.jar* can be loaded from here.*///caller由Reflection.getCallerClass()得到,而调用方是java.sql.DriverManager,所以getClassLoader()是引导类加载器,也就是null//所以此处使用线程上下文加载器来加载实现类ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;synchronized(DriverManager.class) {// synchronize loading of the correct classloader.if (callerCL == null) {callerCL = Thread.currentThread().getContextClassLoader();}}if(url == null) {throw new SQLException("The url cannot be null", "08001");}println("DriverManager.getConnection(\"" + url + "\")");// Walk through the loaded registeredDrivers attempting to make a connection.// Remember the first exception that gets raised so we can reraise it.SQLException reason = null;for(DriverInfo aDriver : registeredDrivers) {// If the caller does not have permission to load the driver then// skip it.// isDriverAllowed中使用给定的加载器加载指定的驱动if(isDriverAllowed(aDriver.driver, callerCL)) {try {println("    trying " + aDriver.driver.getClass().getName());Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!println("getConnection returning " + aDriver.driver.getClass().getName());return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println("    skipping: " + aDriver.getClass().getName());}}// if we got here nobody could connect.if (reason != null)    {println("getConnection failed: " + reason);throw reason;}println("getConnection: no suitable driver found for "+ url);throw new SQLException("No suitable driver found for "+ url, "08001");
}

总结&扩展

  • 双亲委派作为基本模型,隔离了不同的调用者,保证了程序的安全。
  • 线程上线文加载器与其说破坏了双亲委派倒不如说是扩展了双亲委派的能力,使其有更好的通用性。
  • Tomcat、Jetty等Web容器都是基于双亲委派模型来做资源的隔离。
  • Spring在设计中也考虑到了类加载的问题,详细可见:
    org.springframework.web.context.ContextLoader.initWebApplicationContext(…)。

参考

http://www.infocool.net/kb/Tomcat/201609/193323.html
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://github.thinkingbar.com/classloader/

了解ClassLoader相关推荐

  1. JVM系列(之ClassLoader)

    Class Loader Java运作流程 内部class loader bootstrap class loader --引导类加载器,它负责加载Java的核心类[java.* ](如classpa ...

  2. Java中的ClassLoader和SPI机制

    深入探讨 Java 类加载器 成富是著名的Java专家,在IBM技术网站发表很多Java好文,也有著作. 线程上下文类加载器 线程上下文类加载器(context class loader)是从 JDK ...

  3. Classloader内存泄露

    2019独角兽企业重金招聘Python工程师标准>>> 最近遇到了这个问题,在修改了-Xmx后有时仍然会出现,下文分析的很有启发,看了下文重新分析我的应用,在项目中我使用了sprin ...

  4. 如何快速写一个违背双亲委托机制的classloader

    很多情况下,不得以必须写个classloader来满足需求.例如你一个工程里你想用相同的数据库的多个版本,自己制定了一个jar包目录,没有classloader管理等等.如果是一个遵循java已经规定 ...

  5. ClassLoader知识收集

    阅读提示: 全文认真阅读大约需要1个半小时时间,如果你需要在IDE中验证并理解,大约需要3个小时,如果你想自己写个类似的类加载器并调试,估计还需要3个小时. 该知识点的掌握检测与否,你可以尝试其回答J ...

  6. Class.forName 和 ClassLoader 到底有啥区别?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | 纪莫 来源 | https://www.cnblogs. ...

  7. 面试题:Class.forName 和 ClassLoader 有什么区别?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://t.cn/AiQQ7dwi 在 java 中 ...

  8. Java基础—ClassLoader的理解

    ##默认的三个类加载器 Java默认是有三个ClassLoader,按层次关系从上到下依次是:- Bootstrap ClassLoader- Ext ClassLoader- System Clas ...

  9. java的classloader引用实例_通过实例Java ClassLoader原理

    注:本文是个人对java虚拟机规范提到的知识的一点总结. 在Java中,类必须经过jvm使用类装载器(class loader)装载(load)之后才能使用.以主程序(Class A)为例,当jvm调 ...

  10. 利用classloader同一个项目中加载另一个同名的类_线程上下文类加载器ContextClassLoader内存泄漏隐患...

    前提 今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器Cont ...

最新文章

  1. 论文阅读工具ReadPaper
  2. python基础知识整理-python入门基础知识点整理-20171214
  3. Mysql| Mysql函数,聚集函数的介绍与使用(Lower,Date,Mod,AVG,...)
  4. 工作32:get之前打印
  5. java分治法求数列的最大子段和_同事为进大厂天天刷Java面试题,面试却履败!究其原因竟是它在捣鬼。...
  6. 大型网站架构演进(4)使用应用服务器集群
  7. android在activity之间传递map类型值
  8. 【控制】动力学建模举例 --> 牛顿-欧拉法
  9. PS-elevenday-仿制图章工具组
  10. 关于taocp的MIX[水上原创]
  11. springboot自动装配原理
  12. 读卡器插电脑不显示盘符
  13. jQuery入门(一)--jQuery中的选择器
  14. 【安全】椭圆曲线加密算法(ECC)深入理解
  15. solidity的函数修改器(modifier)
  16. 计算机病毒教学评课,计算机病毒评课稿.doc
  17. 新年第一帖——元旦这天骑车迷路了
  18. 牛批!Alibaba内部学习指南+最新面试题+学习大纲+内部学习书籍,理论与实战双管齐下!
  19. lubuntu12.04将64G minSD卡 格式exFAT 转 FAT32
  20. ubuntu下搜狗输入法为乱码

热门文章

  1. 蓝海卓越计费管理系统 debug.php 远程命令执行漏洞
  2. php伪协议实现命令执行,任意文件读取
  3. 半导体物理 第七章 金属半导体接触及其能级图
  4. SpringBoot2 整合 JWT 框架,解决Token跨域验证问题
  5. 如何深度解析Python面向对象
  6. 森林门前的小路用计算机弹奏歌曲,森林外的小路看花香漫步什么歌
  7. 交通局信息上报“二次录入”难题交给博为小帮!
  8. 求解三维装箱问题的启发式深度优先搜索算法(python)
  9. w7修复计算机,如何修复Win7系统?Win7系统修复教程
  10. 类EMD的“信号分解方法”及MATLAB实现(第七篇)——EWT