先来一道题,试试水平

  1. public static void main(String[] args) {
  2. ClassLoader c1 = ClassloaderStudy.class.getClassLoader();
  3. ClassLoader c1Parent = ClassloaderStudy.class.getClassLoader().getParent();
  4. ClassLoader c1ParentParent = ClassloaderStudy.class.getClassLoader()
  5. .getParent().getParent();
  6. ClassLoader currentThreadClassloader = Thread.currentThread()
  7. .getContextClassLoader();
  8. ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
  9. //下面打印的结果是什么?
  10. System.out.println(c1);
  11. System.out.println(c1Parent);
  12. System.out.println(c1ParentParent);
  13. System.out.println(c1 == currentThreadClassloader);
  14. System.out.println(c1 == systemClassLoader);
  15. }

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

/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

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

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

sun.misc.Launcher$ExtClassLoader

  1. System.out.println(System.getProperty("java.ext.dirs"));
  2. //当然你也可以通过这个方法指定扩展类加载器加载的路径
  3. 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的源代码中,我们看如下代码:

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

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

  1. //resolve 这个字段表示加载类时是否进行链接操作,默认否
  2. protected Class<?> loadClass(String name, boolean resolve){
  3. //判断该类是否已经加载
  4. Class<?> c = findLoadedClass(name);
  5. if (c == null) {
  6. try {
  7. //如果父类加载器不为空,调用父类的loadClass方法
  8. if (parent != null) {
  9. c = parent.loadClass(name, false);
  10. } else {
  11. c = findBootstrapClassOrNull(name);
  12. }
  13. } catch (ClassNotFoundException e) {
  14. //如果父类加载失败
  15. }
  16. //调用子类的findClass方法
  17. if (c == null) {
  18. c = findClass(name);
  19. }
  20. }
  21. //如果为true,则进行链接操作
  22. if (resolve) {
  23. resolveClass(c);
  24. }
  25. //返回加载后的字节码
  26. return c;
  27. }

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

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

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

  1. private static native Class<?> forName0(String name, boolean initialize,
  2. ClassLoader loader,
  3. Class<?> caller)
  4. 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,那么就会用到线程上下文类加载器。

  1. //核心就是下面,省略了安全代码
  2. public void setContextClassLoader(ClassLoader cl) {
  3. contextClassLoader = cl;
  4. }
  5. public ClassLoader getContextClassLoader() {
  6. return contextClassLoader;
  7. }

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

假设小明遇到的问题如下,你的项目中需要引入两个三方组件:消息中间件(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. easyUI dataGrid主从表点击展开问题

    昨天在公司写代码遇到了一个问题,就是在用easyUI做主从表的时候在查询之后点击展开的时候不能再次展开了.先说一下主从表我也是第一次用 效果如下图: 然后点击前面的小加号出现以下效果: 然而遇到了一个 ...

  2. 使用svndumpfilter exclude来清理svn库的废弃文件实现差别备份

      先啰嗦下为什么要使用svndumpfilter… svn库用久了以后就会越来越大,进行整体文件打包备份的时候,发现压力山大…尤其是美术团队也在使用svn进行重要美术资源管理的时候…….几百g的资源 ...

  3. nginx部署vue跨域proxy方式

    server { listen 80; charset utf-8; #server_name localhost; server_name you_h5_name; ###VUE项目H5域名 err ...

  4. 20191031-3 beta week 1/2 Scrum立会报告+燃尽图 03

    此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/9913 git地址:https://e.coding.net/Eustia ...

  5. 【汇编】1.汇编环境的搭建:DOSBox的安装

    前言 DOSBox是一款在windows系统运行DOS程序的环境模拟器.可以解决在64位机中汇编程序编译调试等问题. 本文以 DOSBox 0.74 为例,汇编编译程序采用MASM6. 第一步下载相关 ...

  6. 1047 编程团体赛 (20 分)C语言

    编程团体赛的规则为:每个参赛队由若干队员组成:所有队员独立比赛:参赛队的成绩为所有队员的成绩和:成绩最高的队获胜. 现给定所有队员的比赛成绩,请你编写程序找出冠军队. 输入格式: 输入第一行给出一个正 ...

  7. 还看不懂同事代码?快来补一波 Java 7 语法特性

    前言 Java 平台自出现到目前为止,已经 20 多个年头了,这 20 多年间 Java 也一直作为最流行的程序设计语言之一,不断面临着其他新兴编程语言的挑战与冲击.Java 语言是一种静态强类型语言 ...

  8. 在EasyUI项目中使用FileBox控件实现文件上传处理

    我在较早之前的随笔<基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用>Web框架介绍中介绍了基于Uploadify的文件上传操作,免费版本用的是J ...

  9. 6.6 hadoop作业调优

    提高速度和性能.可以从下面几个点去优化 可以在本地运行调试来优化性能,但是本地和集群是完全不同的环境,数据流模式也截然不同,性能优化要在集群上测试.有些问题如(内存溢出)只能在集群上重现. HPROF ...

  10. Session是怎么实现的?存储在哪里?

    为什么有session? 首先大家知道,http协议是无状态的,即你连续访问某个网页100次和访问1次对服务器来说是没有区别对待的,因为它记不住你. 那么,在一些场合,确实需要服务器记住当前用户怎么办 ...