分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!大家可以看看是否对自己有帮助, 点击这里查看【人工智能教程】 。接下来进入正文。
勿在流沙筑高台,出来混迟早要还的。
上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有一个大致的认识,本文会深入到类加载器源码分析,了解类加载器ClassLoader中核心的源码,并且分析ClassLoader中的设计思想或者设计模式!
本文地图:
当你要学习类的时候,首先我们要做的就是这个类API文档,下面我们从JDK API文档对 ClassLoader进行一个简单的整理。在看API之前,我们先可以大致看一下整个ClassLoader的类结构图:
类 ClassLoader在java.lang 包中,是一个抽象类:
public abstract class ClassLoaderextends Object{}复制代码
类加载器是负责加载类的对象。 ClassLoader
类是一个抽象类。如果给定类的,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。
每个 Class
对象都包含一个对定义它的 ClassLoader
的 引用
)。 (这个引用很有用,在类加载器消亡的时候会卸载由它加载的类对象。)
数组类的 Class
对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。
注意:在来加载中, 有些类可能并非源自一个文件;它们可能源自其他来源(如网络),也可能是由应用程序构造的。 defineClass
) 方法将一个 byte 数组转换为 Class
类的实例。这种新定义的类的实例可以使用 Class.newInstance
) 来创建。
类加载器所创建对象的方法和构造方法可以引用其他类。为了确定引用的类,Java 虚拟机将调用最初创建该类的类加载器的 loadClass
) 方法。
如果应用程序要扩展Java 虚拟机动态加载类的方式,必须要继承ClassLoader或其子类。
tips: 二进制名称
在JAVA语言规范中定义了,任何作为 String
类型参数传递给 ClassLoader
中方法的类名称都必须是一个二进制名称。例如:
"java.lang.String" "javax.swing.JSpinner$DefaultEditor" "java.security.KeyStore$Builder$FileBuilder$1" "java.net.URLClassLoader$3$1" `
---
// 返回委托的父类加载器。 ClassLoader getParent() // 使用指定的二进制名称来加载类。 Class<?> loadClass(String name) // 使用指定的二进制名称查找类。 protected Class<?> findClass(String name) // 将一个 byte 数组转换为 Class 类的实例。 protected Class<?> defineClass(String name, byte[] b, int off, int len) // 链接指定的类 protected void resolveClass(Class<?> c) // 其他的自行查资料学习 复制代码
将完 ClassLoader
后在看一个它的子类 URLClassLoader
,它重写了 ClassLoader
中的一些方法,并且它的子类中有我们比较关注的 AppClassLoader
和 ExtClassLoader
:
URLClassLoader:该类加载器用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。这里假定任何以 '/' 结束的 URL 都是指向目录的。如果不是以该字符结束,则认为该 URL 指向一个将根据需要打开的 JAR 文件。
在看源码之前在回顾一下ClassLoader的作用,这样也能让我们知道从什么地方开始阅读源码!
回顾: 我们编写的Java程序在编译后,必须加载到JVM中才能运行,类装载器所做的工作就是把class文件从指定的地方读取到内存中,JVM在加载类的时候,都是通过类装载器 ClassLoader
的 loadClass()
方法来加载class的, loadClass
使用 双亲委派模式 。具体看下面源码分析:
/** * 使用指定的二进制名称来加载类。此方法使用与 loadClass(String, boolean) 方法相同的方式搜索类。 * Java 虚拟机调用它来分析类引用。调用此方法等效于调用 loadClass(name, false)。 * @throws ClassNotFoundException 如果类没有发现,抛出异常 */ public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } /** * 使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类: * * 1.调用 findLoadedClass(String) 来检查是否已经加载类。 * * 2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。 * * 3.调用 findClass(String) 方法查找类。 * * 如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。 * * 鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法,此方法是一个空方法。 * * @param name 类的二进制名称 * @param resolve 如果该参数为 true,则分析这个类 * @return 得到的 Class 对象 * @throws ClassNotFoundException 如果无法找到类 */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 除非被重写,否则这个方法默认在整个装载过程中都是同步的,使用了synchronized(也就是线程安全的) synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 检查类是否已经被加载 ,如果加载过 直接返回该Class类型的对象 Class<?> c = findLoadedClass(name); // 说明:1 if (c == null) { long t0 = System.nanoTime(); try { // 类未加载 返回 null ,则判断 这个 classLoaer 是否有父类 // 父类不等于 null 就去父类去加载,否则去 BootStrap中去加载,双亲委派模式! 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 //如果找不到类,则抛出ClassNotFoundException //来自非null父类加载器 } if (c == null) { // If still not found, then invoke findClass in order // to find the class. // 如果都没有找到的话,则从findCLass中去找,这个findClass ,子类去复写这个方法 // 默认就是实现自定义的classLoader long t1 = System.nanoTime(); c = findClass(name);// 说明:2 // 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; } } /** * 说明:1 * 如果 Java 虚拟机已将此加载器记录为具有给定二进制名称的某个类的启动加载器, * 则返回该二进制名称的类。否则,返回 null。 * @param name * @return */ protected final Class<?> findLoadedClass(String name) { // checkName(name) 如果名称为null或可能是有效的二进制名称,则返回true if (!checkName(name)) return null; return findLoadedClass0(name); } private native final Class<?> findLoadedClass0(String name); /** * 说明:2 * 使用指定的二进制名称查找类。此方法应该被类加载器的实现重写,该实现按照委托模型来加载类。 * 在通过父类加载器检查所请求的类后,此方法将被 loadClass 方法调用。 * 默认实现抛出一个 ClassNotFoundException。 子类去重写! * @param name 类的二进制名称 * @return * @throws ClassNotFoundException */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 复制代码
AppClassLoader
也重写了 loadClass()
,省略了很多代码!因为 AppClassLoader
继承自 URLClassLoader
, URLClassLoader
中重写了 findClass()
方法!具体感兴趣的伙伴可以自行看相关源码!
static class AppClassLoader extends URLClassLoader { // 省略其他代码.... public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException { int var3 = var1.lastIndexOf(46); // 省略其他代码.... } else { return super.loadClass(var1, var2); } } // 省略其他代码.... }复制代码
看了上面的源码,主要对 loadClass
进行分析!
第一: synchronized (getClassLoadingLock(name))
, 默认在整个装载过程中都是线程安全的 。
synchronized (getClassLoadingLock(name))
看到这行代码,我们能知道的是,这是一个 同步代码块 ,那么synchronized的括号中放的应该是一个对象。看一下具体的源码:
/** * 返回类加载操作的锁定对象。 * private final ConcurrentHashMap parallelLockMap; **/ protected Object getClassLoadingLock(String className) { Object lock = this; if (parallelLockMap != null) { Object newLock = new Object(); lock = parallelLockMap.putIfAbsent(className, newLock); if (lock == null) { lock = newLock; } } return lock; }复制代码
我们来看 getClassLoadingLock(name)
方法的作用是什么:代码中用到变量 parallelLockMap
实际上是一个 ConcurrentHashMap
,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个 putIfAbsent(className, newLock)
方法来给刚刚创建好的对象赋值,(稍后介绍此方法的作用)。那么这个 parallelLockMap
变量又是哪来的那,我们发现这个变量是 ClassLoader
类的成员变量。
private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; // 根据一个属性ParallelLoaders的Registered状态的不同来给parallelLockMap 赋值 if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); // 其他代码省略 ... } else { // no finer-grained lock; lock on the classloader instance // 没有更精细的锁;锁定类加载器实例 parallelLockMap = null; // 其他代码省略 ... } }复制代码
从构造函数中看到,parallelLockMap的值是根据 ParallelLoaders.isRegistered 去进行判断并赋值的,那么,ParallelLoaders是在什么地方赋值的呢?在ClassLoader类中包含一个静态内部类 private static class ParallelLoaders
,在 ClassLoader
被加载的时候这个静态内部类就被初始化。(整个类的源码没怎么看懂,比较底层深入了)
/** *封装了并行的可装载的类型的集合 */ private static class ParallelLoaders { private ParallelLoaders() {} //一组并行的加载器类型 private static final Set<Class<? extends ClassLoader>> loaderTypes = Collections.newSetFromMap( new WeakHashMap<Class<? extends ClassLoader>, Boolean>()); static { synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); } } /** * 将给定的类加载器类型注册为并行capabale。 */ static boolean register(Class<? extends ClassLoader> c) { synchronized (loaderTypes) { if (loaderTypes.contains(c.getSuperclass())) { //将类加载器注册为并行功能 //当且仅当它的所有超类都是。 //注意:给定当前的类加载序列,如果 //直接超级类是并行的, //所有超级上级必须也是。 loaderTypes.add(c); return true; } else { return false; } } } static boolean isRegistered(Class<? extends ClassLoader> c) { synchronized (loaderTypes) { return loaderTypes.contains(c); } } }复制代码
上面这一整段比较乱,简单整理总结:
首先,在ClassLoader类中有一个静态内部类 ParallelLoaders
,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给 parallelLockMap
定义,就是 new
一个 ConcurrentHashMap()
,那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么 parallelLockMap
就不是 Null
,这个时候,我们判断 parallelLockMap
是不是 Null
,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象, getClassLoadingLock
方法直接返回 this
,就是当前的加载器的一个实例。如果这个 parallelLockMap
不是 null
,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用 parallelLockMap
的 putIfAbsent(className, newLock)
方法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。 这个时候,我们来简单说明一下 getClassLoadingLock(String className)
的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。[摘自:参考资料2]
第二: findLoadedClass(name)
,查找类是否已经被加载过,如果加载过 直接返回该Class类型的对象。如果没有被加载则继续第三的操作!
第三: c = findBootstrapClassOrNull(name);
和 c = parent.loadClass(name, false);
如果父加载器不为空,那么调用父加载器的 loadClass
方法加载类,如果父加载器为空,那么调用虚拟机的加载器来加载类。
如果以上两个步骤都没有成功的加载到类,进入第四。
第四: c = findClass(name);
使用自定义的 findClass(name)
方法来加载类。
这个时候,我们已经得到了加载之后的类,那么就根据 resolve
的值决定是否调用 resolveClass
方法。进入第五!
第五: resolveClass(c);
链接指定的类。 这个方法给 Classloader
用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™
规范中的 Execution
描述进行链接……
在ClassLoader中虽然是叫双亲委派(父亲委派)机制,但真实的实现不是以继承的关系进行实现,而是都使用组合关系来复父加载器的代码。通过 getParent()
这个方法设置父类加载器的。这个其实在面向对象(OO)设计原则中有一条就是: 多用组合,少用继承 !
类加载器中用到了模板方法模式: 模板方法模式用于定义构建某个对象的步骤与顺序,或者定义一个算法的骨架 。
模板方法模式的使用的方式,给子类足够的自由度,提供一些方法供子类覆盖,去实现一些骨架中不是必须但却可以有自定义实现的步骤。模板方法模式是一种基础继承的代码复用技术。如ClassLoader中的findClass方法!
public abstract class ClassLoader { //这里就是父类算法的定义 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } //这里留了一个方法给子类选择性覆盖 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }复制代码
在来回顾:在ClassLoader中定义的算法顺序是。
1、首先看是否有已经加载好的类。
2、如果父类加载器不为空,则首先从父类类加载器加载。
3、如果父类加载器为空,则尝试从启动加载器加载。
4、如果两者都失败,才尝试从findClass方法加载。
本文从源码和设计模式的角度对ClassLoader进行分析和学习,更加深入的学习了ClassLoader,也方便后续我们自定义的类加载器,下一篇介绍如果实现一个自定义的类加载器!
tips : 常见异常:
ClassNotFoundException 这是最常见的异常,产生这个异常的原因为在当前的ClassLoader 中加载类时,未找到类文件。
NoClassDefFoundError 这个异常是因为 加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。
LinkageError 该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成该异常。
《JDK API 文档》
深度分析 Java 的 ClassLoader 机制(源码级别):http://blog.jobbole.com/96145/
备注: 由于本人能力有限,文中若有错误之处,欢迎指正。
谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!