勿在流沙筑高台,出来混迟早要还的。

做一个积极的人

编码、改bug、提升自己

我有一个乐园,面向编程,春暖花开!

上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有一个大致的认识,本文会深入到类加载器源码分析,了解类加载器ClassLoader中核心的源码,并且分析ClassLoader中的设计思想或者设计模式!

本文地图:

一、ClassLoader核心API介绍

当你要学习类的时候,首先我们要做的就是这个类API文档,下面我们从JDK API文档对 ClassLoader进行一个简单的整理。在看API之前,我们先可以大致看一下整个ClassLoader的类结构图:

类 ClassLoader在java.lang 包中,是一个抽象类:

public abstract class ClassLoaderextends Object{}

类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。

每个 Class 对象都包含一个对定义它的 ClassLoader 的引用。 (这个引用很有用,在类加载器消亡的时候会卸载由它加载的类对象。)

数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。

注意:在来加载中, 有些类可能并非源自一个文件;它们可能源自其他来源(如网络),也可能是由应用程序构造的。defineClass](../../java/lang/ClassLoader.html#defineClass(java.lang.String, byte[], int, int)) 方法将一个 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"

核心API

// 返回委托的父类加载器。
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核心源码分析

在看源码之前在回顾一下ClassLoader的作用,这样也能让我们知道从什么地方开始阅读源码!

回顾: 我们编写的Java程序在编译后,必须加载到JVM中才能运行,类装载器所做的工作就是把class文件从指定的地方读取到内存中,JVM在加载类的时候,都是通过类装载器ClassLoaderloadClass()方法来加载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())) {
//将类加载器注册为并行功能
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //当且仅当它的所有超类都是。
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //注意:给定当前的类加载序列,如果
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //直接超级类是并行的,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //所有超级上级必须也是。
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对象,调用parallelLockMapputIfAbsent(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设计思想解析

在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进行分析和学习,更加深入的学习了ClassLoader,也方便后续我们自定义的类加载器,下一篇介绍如果实现一个自定义的类加载器!


tips : 常见异常:

附录:

ClassNotFoundException 这是最常见的异常,产生这个异常的原因为在当前的ClassLoader 中加载类时,未找到类文件。

NoClassDefFoundError 这个异常是因为 加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。

LinkageError 该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成该异常。


五、参考资料

《JDK API 文档》

深度分析 Java 的 ClassLoader 机制(源码级别):http://blog.jobbole.com/96145/


谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

博客首页 : http://blog.csdn.net/u010648555

愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人

© 每天都在变得更好的阿飞云

Java内存管理-掌握类加载器的核心源码和设计模式(六)的更多相关文章

  1. 图解kubernetes调度器SchedulingQueue核心源码实现

    SchedulingQueue是kubernetes scheduler中负责进行等待调度pod存储的对,Scheduler通过SchedulingQueue来获取当前系统中等待调度的Pod,本文主要 ...

  2. 图解kubernetes调度器SchedulerCache核心源码实现

    SchedulerCache是kubernetes scheduler中负责本地数据缓存的核心数据结构, 其实现了Cache接口,负责存储从apiserver获取的数据,提供给Scheduler调度器 ...

  3. Java内存管理-掌握自定义类加载器的实现(七)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇分析了ClassLoader的类加载相关的核心源码,也简单介绍了ClassLoa ...

  4. Java内存管理-掌握虚拟机类加载器(五)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇介绍虚拟机类加载机制,讲解了类加载机制中的三个阶段,分别是:加载.连接(验证.准 ...

  5. Java内存管理-掌握虚拟机类加载机制(四)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇介绍了整个JVM运行时的区域,以及简单对比了JDK7和JDK8中JVM运行时区域 ...

  6. Java 内存管理

    java 内存管理机制 JAVA 内存管理总结 java 是如何管理内存的 Java 的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字 new 为 ...

  7. java内存管理机制

    JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 ( ...

  8. java内存管理(堆、栈、方法区)

    java内存管理 简介 首先我们要了解我们为什么要学习java虚拟机的内存管理,不是java的gc垃圾回收机制都帮我们释放了内存了吗?但是在写程序的过程中却也往往因为不懂内存管理而造成了一些不容易察觉 ...

  9. Java内存管理及对Java对象管理

    Java内存管理及对Java对象管理 1Java内存管理 1.1Java中的堆和栈 通常来说,人们会将Java内存氛围栈内存(Stack)和堆内存(Heap). 栈内存用来保存基本类型的变量和对象的引 ...

随机推荐

  1. day04 运算符 流程控制 (if while/of)

    1. 运算符算数运算符 + - * / int / float :数字类型 # print(10 + 3.1)# print(10 / 3)# print(10 // 3)# print(10 % 3 ...

  2. 水果(map的嵌套)

    夏天来了~~好开心啊,呵呵,好多好多水果~~ Joe经营着一个不大的水果店.他认为生存之道就是经营最受顾客欢迎的水果.现在他想要一份水果销售情况的明细表,这样Joe就可以很容易掌握所有水果的销售情况了 ...

  3. Python基础之函数二

    函数的嵌套 通过名字就能理解,函数里是还可以套着函数用的.这么牛,下面就来看看几段代码,看看是怎么回事.注意:函数一定是先定义后使用. x=1234 def f1(): #定义一个主函数 x = 1 ...

  4. less 写关键帧动画

    @keyframes 关键帧动画写在less里的时候,务必要写在所有的{}之外,不能被{}包裹 甚至务必写在代码的最后 不然报错 Compilation resulted in incorrect C ...

  5. 提取Word里的文本内容 C#

    using DocumentFormat.OpenXml.Packaging; public static string TextFromWord(string path) { const strin ...

  6. 51Nod 1298 圆与三角形(计算几何)

    1298 圆与三角形  基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题 给出圆的圆心和半径,以及三角形的三个顶点,问圆同三角形是否相交.相交输出"Yes&quo ...

  7. How does exercise keep your brain young?

    Exercise may protect the brain from disease and dementia as we age, but the mechanisms behind its be ...

  8. mac pro 基本使用

    command+空格  调出搜索框 可以输入:网络,就是配置ip等信息 可以输入:终端,就是命令行窗口 可以输入:触控板,可以调节多点触控版 可以输入:系统偏好设置,进行配置 触控板:多点触控——两个 ...

  9. FATAL Fatal error during KafkaServerStable startup. Prepare to shutdown (kafka.server.KafkaServerStartable) java.io.FileNotFoundException: /tmp/kafka-logs/.lock (Permission denied)

    1.启动kafka的时候,报错如下所示: [-- ::,] INFO zookeeper state changed (SyncConnected) (org.I0Itec.zkclient.ZkCl ...

  10. lojround6

    花团 线段树分治裸题 给出了结束时间跟离线没区别 「LibreOJ Round #6」花火 首先在第一次使用交换是显然的 然后统计逆序对暴力是n^2的(前缀和优化) 因为交换两个点改变的只有x< ...