先来一道题,试试水平

public static void main(String[] args) {
ClassLoader c1 = ClassloaderStudy.class.getClassLoader();
ClassLoader c1Parent = ClassloaderStudy.class.getClassLoader().getParent();
ClassLoader c1ParentParent = ClassloaderStudy.class.getClassLoader()
.getParent().getParent();
ClassLoader currentThreadClassloader = Thread.currentThread()
.getContextClassLoader();
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //下面打印的结果是什么?
System.out.println(c1);
System.out.println(c1Parent);
System.out.println(c1ParentParent);
System.out.println(c1 == currentThreadClassloader);
System.out.println(c1 == systemClassLoader);
}

上面的打印结果你猜对了吗?

/D:/github/java_common/target/classes/

sun.misc.Launcher|AppClassLoader@18b4aac2

sun.misc.Launcher|ExtClassLoader@1a86f2f1

null

true

true

类加载器都有哪些

JVM类加载器总共有三种,每种类加载器的职责和实现上都不一样,不同的类加载器负责不同类路径的加载,列表如下:

  1. 根类加载器 (BootstrapClassloader)
  2. 扩展类加载器 (ExtensionClassloader)
  3. 系统类加载器 (ApplicationClassloader)

BootstrapClassloader主要负责Java的核心类加载(jre/lib/..),用C++实现的,它并没有继承Classloader,通常它也叫做引导类加载器,设计到虚拟机的实现细节,不允许开发者直接获取到根类加载器的引用,在执行java的命令中使用-Xbootclasspath选项来扩展根类加载器的加载路径或者重新指定路径

  1. -Xbootclasspath: 完全取代基本核心的Java class 搜索路径.不常用,否则要重新写所有Java 核心class
  2. -Xbootclasspath/a: 后缀在核心class搜索路径后面.常用!!
  3. -Xbootclasspath/p: 前缀在核心class搜索路径前面.不常用,避免引起不必要的冲突.

比如我在ide中的配置,我需要配置的cldrdata.jar在核心class搜索路径的后面,所以配置代码如下

-Xbootclasspath/a:D:/sdk/jdk8/jre/lib/ext/cldrdata.jar

//获取根类加载器的加载的路径
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}

ExtensionClassloader主负责jre的扩展目录jar加载(jre/ext/...),或者你可以通过一个属性来指定(java.ext.dirs)哪个目录被扩展类加载器加载,它是由Java语言实现的,下面代码可以获取扩展类加载器加载的路径,开发人员可以使用这个类加载器,它的实现代码位置位于sun.misc包下,这个类是继承java.lang.Classloader类的,如下:

sun.misc.Launcher$ExtClassLoader

System.out.println(System.getProperty("java.ext.dirs"));
//当然你也可以通过这个方法指定扩展类加载器加载的路径
System.setProperty("java.ext.dirs","value");

ApplicationClassloader主要负责加载用户类路径(classpath)所指定的类,开发人员可以使用这个类加载器,你可以通过属性(java.class.path)获取由该类加载器加载的路径,同时你可以通过这个属性设置该类加载器加载的路径,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,这个类也是继承java.lang.Classloader类的,系统类加载器的源码实现地址如下:

sun.misc.Launcher$AppClassLoader

JVM的类加载器机制是什么

JVM的类加载机制主要分为三种,这三种类加载机制相互配合,保证了JVM类加载的完整性,正确性,可扩展性。当然也有缺点。

  1. 全盘负责,但某个class文件被一个类加载器加载的时候,该class文件所依赖的class和所引用的class文件都将由这个类加载器进行加载。除非你显示的用代码来使用另一个类加载器来操作。
  2. 双亲委托,当类加载器加载一个class文件时,总是先询问自己的父类加载器是否能够加载这个class文件,如果自己的父类加载器可以加载,那么就交给父类加载器,如果父类不可以,则自己加载。
  3. 缓存机制,缓存机制保证所有被加载过的class文件都会被缓存,当程序中需要使用某个class文件时先从缓存中获取,缓存中没有时才会加载,这样会保证一个class文件只会被加载一次。

一单一个类被加载到JVM中,就不会被再次加载了,在JVM中类的唯一标识是加载该类的类加载器加上该类的全限定类名。在JAVA中类的标识是类的全限定类名,这两个是有点不一样的,大家记住了。

类加载机制的优点

这样的类加载机制使得类加载有了层次和优先级的关系,这种关系可以避免类的重复加载,可以保证类加载的安全(Java核心API不被随意替换),例如类java.lang.Integer,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Integer类在程序的各种类加载器环境中都是同一个类。否则,你想一想,如果用户自己写了一个名为java.lang.Integer的类,并放在程序的classpath中,那系统中将会出现多个不同的Integer类,Java类型体系中最基础的行为也无法保证,Integer将会被多个不同的类加载器加载,应用程序也会变得一片混乱。

类加载机制的缺点

上面描述的类加载机制看似完美,但真的如此吗?双亲委派机制总共被破坏过三次,这正是它的缺点所导致的结果,我们一一来看。

第一次:双亲委派模型时出现在jdk1.2版本的,但在jdk1.2之前呢?java.lang.ClassLoader是在1.0时候就存在的,面对已经存在的用户自定义类加载器的实现代码,Java开发者们是这样的设计的,jdk1.2的时候在Classloader类中添加了一个方法(findClass),如果你看过源代码,你会发现这个这个抛出了一个异常throws ClassNotFoundException,为什么呢?1.2之前,开发者们去继承Classloader的唯一目的就是重新loadClass()方法使得使用自定义的类加载器,那么1.2之后,使用了双亲委派模型,开发者需要重写的是findClass()方法,因为在loadClass()方法中,如果父加载器加载失败后,会调用子类的findClass()方法,这样就保证了双亲委派模型,这就是也是双亲委派模型的实现。

第二次:Java应用程序中一般都是上层调用下层,核心API总是被作为最底层来提供服务,它们总是基础,那么有没有可能基础调用上层,比如Integer类调用开发人员写的Java类呢,这是有可能的事情,一个典型的例子就是JNDI,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC等.

第三次:由于用户对程序的动态性的追求导致的(模块化动态部署,升级,卸载),例如OSGI的出现。在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构,OSGi 是目前动态模块系统的事实上的工业标准,它适用于任何需要模块化、面向服务、面向组件的应用程序,蚂蚁的SOFA中间件就是用了OSGI,SOFA已经开源了, 自行了解OSGI。

类加载机制的实现

在Classloader的源代码中,我们看如下代码:

public Class<?> loadClass(String name)  {
return loadClass(name, false);
}

loadClass又调用了本类的一个重载方法,代码如下

//resolve 这个字段表示加载类时是否进行链接操作,默认否
protected Class<?> loadClass(String name, boolean resolve){
//判断该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//如果父类加载器不为空,调用父类的loadClass方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载失败
}
//调用子类的findClass方法
if (c == null) {
c = findClass(name);
}
}
//如果为true,则进行链接操作
if (resolve) {
resolveClass(c);
}
//返回加载后的字节码
return c;
}

开发人员我们常用的类加载的方法主要有两种,这两种方法有一些区别,我们一起来看看他们区别在哪里呢?

Class.forName(String className),Classloader.loadClass(String className)

这两个方法的入参都是类的全限定类名,两个方法都被重载了,重载后的如下方法如下,我们可以看到,重载方法入参都有boolean参数,前者默认值是true,代表默认进行初始化,后者默认值时flase,代表默认不进行链接操作。这下清楚了吧。

private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
protected Class<?> loadClass(String name, boolean resolve)

再来说说URLClassloader的作用,URLClassloader继承了Classloader,它提供了什么新的作用吗,其实Ext和App的Classloader是继承了URLClassloader的,一般动态加载类都是直接用Class.forName()这个方法,但这个方法只能创建程序中已经引用的类,并且只能用包名的方法进行索引,比如Java.lang.String,不能对一个.class文件或者一个不在程序引用里的.jar包中的类进行创建。URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:

  • 文件: (从文件系统目录加载)
  • jar包: (从Jar包进行加载)
  • Http: (从远程的Http服务进行加载)

线程上下文类加载器,其实上面已经提到了这个设计的引入的作用,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到,比如在dubbo中的核心实现就是SPI,那么就会用到线程上下文类加载器。

//核心就是下面,省略了安全代码
public void setContextClassLoader(ClassLoader cl) {
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}

什么是类隔离能力,怎么实现

假设小明遇到的问题如下,你的项目中需要引入两个三方组件:消息中间件(A)和和微服务中间件(B),组件A需要依赖guava19.0,组件B需要依赖guava23.0,因为guava19.0和guava23.0 是不兼容的,怎么办?

作为开发者,遇到这种包冲突问题,如果不借助类隔离框架,只能耗费精力升级到统一版本

所谓类隔离就是应用程序中不同的包使用不同的类加载进行加载,比如消息中间件使用M类加载器加载,微服务使用N类加载器加载,这样guava19.0和guava.23会被不同的类加载加载从而实现jar通途解决。看到这可能有人问了guava不久被加载两次了啊?上面说过,JVM类加载中类加载的唯一标识是类加载+类的全限定类名。

蚂蚁金服开源了一个框架SOFAArk,他是一个轻量级的Java类加载隔离框架,使用Java语言进行开发的。他的原理就是通过独立的类加载器加载相互冲突的三方依赖包,从而做到隔离包冲突,怎么实现呢?

原因是Ark Plugin,它是 SOFAArk 框架定义的一种特殊的JAR包文件格式,在遇到包冲突时,用户可以使用Maven插件将若干冲突包打包成Plugin,运行时由独立的 PluginClassLoader加载,从而解决包冲突.

还有Tomcat的应用间的类加载隔离能力,比如:在一个Tomcat内部署多个应用,甚至多个应用内使用了某个类似的几个不同版本,但它们之间却互不影响。这是如何做到的,原因是当一个应用启动的时候,会为其创建对应的WebappClassLoader(本质是上下文类加载器),细节不在这里说了。

用一张图总结

扫描下方二维码,关注公众号:技术人技术事 ,阅读更多精彩文章,一起交流。

![](https://img2018.cnblogs.com/blog/706455/201909/706455-20190911210708072-261554801.jpg)

深入JVM类加载器机制,值得你收藏的更多相关文章

  1. (二十七)JVM类加载器机制与类加载过程

    一.Java虚拟机启动.加载类过程分析 下面我将定义一个非常简单的java程序并运行它,来逐步分析java虚拟机启动的过程. package org.luanlouis.jvm.load; impor ...

  2. Tomcat类加载器机制

    Tomcat为什么需要定制自己的ClassLoader: 1.定制特定的规则:隔离webapp,安全考虑,reload热插拔 2.缓存类 3.事先加载 要说Tomcat的Classloader机制,我 ...

  3. JVM类加载器的分类

    类加载器的分类 JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader). 从概念上来讲,自定 ...

  4. JVM 类加载器的双亲委托机制

    1.类加载器的层次结构 在双亲委托机制中,各个加载器按照父子关系形成了树形结构(逻辑意义),除了根加载器之外,其余的类加载器都有且只有一个父加载器. public class MyTest13 { p ...

  5. jvm类加载器以及双亲委派

    首先来了解几个概念: 类加载: 概念:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验--转换解析--初始化,最终形成能被java虚拟机直接使用的java类型,就是jvm的类加载机制. ...

  6. JVM 类加载器 (二)

    1.类加载器(ClassLoader)负责加载class文件,class文件在文件开头有特定的文件标识,并且ClassLoader只负责 class 文件的加载,至于class文件是否能够运行则由Ex ...

  7. JVM类加载器及Java类的生命周期

    预定义类加载器(三种): 启动(Bootstrap)类加载器: 是用本地代码实现的类装入器,它负责将<Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar) ...

  8. JVM类加载器工作流程

    类加载器 classloader:谈到类加载,不得不提的就是负责此项工作的类加载器classloader,classloader的职责是将Java源文件编译后的字节码文件加载到内存中去执行. 类加载至 ...

  9. 彻底搞懂JVM类加载器:基本概念

    本文阅读时间大约9分钟. 写在前面 在Java面试中,在考察完项目经验.基础技术后,我会根据候选人的特点进行知识深度的考察,如果候选人简历上有写JVM(Java虚拟机)相关的东西,那么我常常会问一些J ...

随机推荐

  1. SpringBoot Starter机制 - 自定义Starter

    目录 前言 1.起源 2.SpringBoot Starter 原理 3.自定义 Starter 3.1 创建 Starter 3.2 测试自定义 Starter 前言         最近在学习Sp ...

  2. linux 更新jdk

    1.上传jdk版本的包 下载JDK地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.h ...

  3. 如何让接口文档自动生成,SpringBoot中Swagger的使用

    目录 一.在SpringBoot项目中配置Swagger2 1.pom.xml中对Swagger2的依赖 2.编写配置类启用Swagger 3.配置实体类的文档 4.配置接口的文档 5.访问文档 二. ...

  4. 「Vijos 1284」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔法阵

    佳佳的魔法阵 背景 也许是为了捕捉猎物(捕捉MM?),也许是因为其它原因,总之,佳佳准备设计一个魔法阵.而设计魔法阵涉及到的最关键问题,似乎就是那些带有魔力的宝石的摆放-- 描述 魔法阵是一个\(n ...

  5. Python学到什么程度可以面试工作(解答一)

    本文整理了 26 个 Python 有用的技巧,将按照首字母从 A~Z 的顺序分享其中一些内容. all 或 any 人们经常开玩笑说 Python 是“可执行的伪代码”,但是当你可以这样编写代码时, ...

  6. ACM北大暑期课培训第三天

    今天讲的内容是深搜和广搜 深搜(DFS) 从起点出发,走过的点要做标记,发现有没走过的点,就随意挑一个往前走,走不 了就回退,此种路径搜索策略就称为“深度优先搜索”,简称“深搜”. bool Dfs( ...

  7. vim的常用指令(脑图)

    将正在编辑的文件另存新文件名   :w newfilename 在正在编辑的文件中,读取一个filename    :r filename 做了很多编辑工作,想还原成原来的文件内容   :e! 我在v ...

  8. Java Data类

    Date类的概述 java.util,Date 表示日期和时间的类类 Date 表示特定的瞬间,精确到千分之一秒(毫秒) 获取时间原点到当前系统时间经历了多少秒 // 时间原点:1970 年 01 月 ...

  9. 使用C#交互快速生成代码!

    #r "System.Reflection" #r "D:\xk.erp\OP.Model\bin\Debug\OP.Model.dll" using Syst ...

  10. 跟着知识追寻者学BeautifulSoup,你学不会打不还口,骂不还手

    一 前言 Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库:其强大的提取能力让知识追寻者放弃了使用正则匹配查找HTML节点:Beautifu Soup 其能直接 ...