文章目录

  • 前言
  • 类加载的概述
    • 双亲委派加载机制
    • 类加载的隔离机制
    • contextClassLoader
      • SPI用处
      • 找文件用处
  • 类加载的顺序
    • 顺序概述
    • 类加载的一般方式
    • 类加载的触发点
  • 类的实例化
    • 多线程环境下,为何也只有一个Class的对象
  • 图解和举例
    • 普通Java应用
    • 日常Web应用
  • 附录

前言

网上有很多的Java类加载机制的介绍, 但是对于初学者而言看起来都太过于深疏, 因此在本文用图解和例子的方式为本文的读者介绍Java的类加载机制。

类加载的概述

双亲委派加载机制

委派模型介绍:

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

通俗的理解就是:

  1. 遇见一个类需要加载的类,它会优先让父加载器去加载。层层传递。
  2. 每个类加载器都有自己的加载区域,它也只能在自己的加载区域里面寻找。
  3. 自定义类加载器也必须实现这样一个双亲委派模型。
  4. 双亲委派机制是隔离的关键, 如String.class
    • 一个JVM里面只能有一个String.class
    • 用户没法自定义个String.class出来。
    • 每个Classloader都有自己的加载区域,需要注意部分配置文件的存放地点。

代码理解:

URLClassLoader loader = (URLClassLoader) Init.class.getClassLoader();
while (loader != null) {System.out.println(loader.getClass().getName() + " 加载的路径:");URL[] urls = loader.getURLs();for (URL url : urls)System.out.println(url);System.out.println("----------------------------");loader = (URLClassLoader)loader.getParent();
}System.out.println("BootstrapClassLoader加载路径: ");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {System.out.println(url);
}

输出(有删减):

sun.misc.Launcher$AppClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/charsets.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/deploy.jar
file:/Users/baidu/workspace/qyp/job/target/classes/
file:${M2_HOME}/org/apache/zookeeper/zookeeper/3.3.6/zookeeper-3.3.6.jar
file:${M2_HOME}/com/alibaba/fastjson/1.2.7/fastjson-1.2.7.jar
----------------------------
sun.misc.Launcher$ExtClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/cldrdata.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/dnsns.jar
----------------------------
BootstrapClassLoader加载路径:
file:${JAVA_HOME}/Contents/Home/jre/lib/resources.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/rt.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/sunrsasign.jar

可以看出:

  • BootstrapClassLoader加载${JAVA_HOME}/jre/lib 下面的部分jar包。比如java.*、sun.*
  • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包。比如javax.*
  • AppClassLoader加载用户classpath下面的jar包。
  • 如果自定义了classloader, 在符合双亲委派模型的基础上,它加载用户自定义classpath下的jar包, 例如tomcat的WEB-INF/classWEB-INF/lib.

类加载的隔离机制

通过不同的 完整类名 和 classloader, 可以区分两个类。好处为内存隔离(最常见的就是静态变量)。

  • 类名不一致一定不是同一个类
  • 类名一致类加载器不一致也不是同一个类(eaquels false)
  • 类名一致类加载器一致但是类加载器实例不一致也不是同一个类。

针对最后一点:类Foo.class, 如果ClassLoader loader1 = new URLClassLoader();ClassLoader loader2 = new URLClassLoader(); loader1和loader2去加载类Foo.class, 得到的Class也不是一个类。


且看问题:在web应用中假如部署了多个webapp. 为了方便共享就预先在Tomcat lib里面内置了部分类比如Spring、JDBC。而用户自备也有类似的Jar包。 这样会引起什么样的冲突?

答案是不会冲突。

Tomcat提供了一个Child优先的类加载机制:首先由子类去加载, 加载不到再由父类加载。就很好的规避了这个问题。WEB-INF/lib 目录下的类的加载优先级是优于Tomcat lib的。(配置文件在server.xml里面的<Loader delegate ="false"/> default false)上。 可见代码片段:
WebappClassLoaderBase#loadClass

boolean delegateLoad = delegate || filter(name, true);

针对Tomcat, 做一个加载路径的介绍:

  • Tomcat起始于catalina.sh里面的命令 java org.apache.catalina.startup.Bootstrap start
  • 因为显式的指定了java命令,因此
    • BootstrapClassLoader负责加载${JAVA_HOME}/jre/lib部分jar包
    • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包
    • AppClassLoader加载bootstrap.jartomcat-juli.jar (只显示的指定了这两个jar包)
    • 之后Tomcat通过初始化了三个URLClassLoader, 并指定加载路径 (见catalina.properties#common.loader配置)
    • 除了common外, server和shardLoader的加载路径一般都没有显示的指定, 因此这三个Loader实际上都是URLClassLoader。
    • 同时,它顺便指定了当前线程的contextClassLoader(讲解见下面小节)。
    • Tomcat对于WEB应用的启动都是依赖于web.xml的, 里面配置的Filter、Listener、Servlet根据Tomcat的定义都是由WebappClassLoaderBase来加载的。
    • 毕竟Filter、Listener、Servlet等入口都是被WebappClassLoaderBase加载的,而一般开发者不会主动指定ClassLoader。那么除非指定了ClassLoader,所有的webapp都是它加载的(刚好它的加载空间包含了这些类)
    • 在需要Spring的时候已经由App自身加载得到, 就不会再去寻找Tomcat lib里面的Spring。
  • 自此,Tomcat的类加载区分完毕。 通过 “子优先” 这个机制,可以保证多个 Tomcat App 之间做到良好的隔离。

contextClassLoader

Thread.currentThread().getContextClassLoader()一般有两个用处:给SPI用, 找配置文件用。

SPI用处

之前讲解过java的委托加载机制1

UserClassLoader -> AppClassLoader->ExtClassLoader -> Bootstrap

委派链左边的ClassLoader就可以很自然的使用右边的ClassLoader所加载的类。

情况反过来,右边的ClassLoader所加载的代码需要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,双亲委托机制是单向的,没办法反过来从右边找左边。

ServiceLoader.load(Class.class); 在加载类的时候, ServiceLoader由BootStrap加载,而一般的SPI都是在用户的classpath下。鉴于方法调用默认是使用的调用类的ClassLoader去加载, 显然BootStrap是加载不了没在它的路径下的Class的, 这个时候就可以传入一个Thread.currentThread().getContextClassLoader(), 就可以很轻松的找到资源文件.

找文件用处

这个跟上诉的SPI机制其实也差不多, 都是每个ClassLoader负责一定的区域, 如果当前区域找不到再使用线程的Loader去找。
比如在Tomcat中执行一个 new File(), 会不会发现文件到${catalina.home}/bin里面去了?

类加载的顺序

当需要用一个类的时候, 必须先加载它。

顺序概述

老生常谈:

  1. 装载:查找和导入Class文件;
  2. 链接:把类的二进制数据合并到JRE中;
    • 校验:检查载入Class文件数据的正确性;
    • 准备:给类的静态变量分配存储空间;
    • 解析:将符号引用转成直接引用;
  3. 初始化:对类的静态变量,静态代码块执行初始化操作

解读(Useless.class为例):

public class Useless {public Serializable s1 = new Serializable() {{System.out.println("域变量");}};public static Serializable s2 = new Serializable() {{System.out.println("静态域变量");}};public static int num = 3;static {System.out.println("静态代码块");}{System.out.println("代码块");}}
  • 装载即通过查找 Useless.class, 得到二进制码。并生产出该类的数据结构,得到一个Class对象。
  • 校验即校验二进制码的数据,比如编译级别、是否符合Java规范等等
  • 准备即为 s2 和 num 赋值 null 和 0。
  • 解析可以参考这篇文字java – JVM的符号引用和直接引用
  • 初始化即另s2得到值,令num得到3。

可以看到, 类加载的整个过程跟域变量和代码块都是没什么关系的

类加载的一般方式

方式一:
Class.forName
方式二
ClassLoader.loadClass

见代码片段:

Class z;
z =  Class.forName("Useless");   // 1
z = Class.forName("Useless", true, MainFather.class.getClassLoader());  // 2
z = Class.forName("Useless", false, MainFather.class.getClassLoader());  // 3z = MainFather.class.getClassLoader().loadClass("Useless");   // 4

一般理解为 1, 3, 4 等价。2会初始化类里面的静态元素和静态代码块(即类加载的初始化步骤)。

类加载的触发点

摘自http://www.cnblogs.com/ITtangtang/p/3978102.html

  (1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。(3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

类的实例化

类只有在加载进入JVM之后才能被使用, 但是一般情况下还需要把类做实例化操作后来用。 一般区分为显示的实例化和隐式的示例化。

类的实例化的目的是为了得到一个类的对象。

  • new 方法为隐式的实例化。方便可用性高。
  • newInstance()为显式的实例化。必须要一个无参构造器。
  • 两种实例化方式都需要ClassLoader的参与。显示的实例化往往指定,隐式的实例化则默认是调用的这个类的类加载器。
  • 实例化的目的都是为了得到类的对象。而类在实例化之前已经初始化完毕。
  • 实例化的时候会为域变量赋值,并执行代码块的方法。

多线程环境下,为何也只有一个Class的对象

2019年04月11日
闲来无事翻了翻老文章。 发现忘了介绍Java类加载如何保证在内存里面只有这一个类对象的。
声明 这个题目是个伪命题,如果自研ClassLoader, 然后还不符合规范去实现, JVM里面肯定是会有多个Class的对象的。
本小节只讲多线程环境下的Case。

BootStrap ClassLoader

  既然是类加载,在双亲委派模型下, 类似于 ”rt.jar“ 一类的类文件, BootStrapClassLoader由 加载。具体源码没有翻过, 不过main() 函数里面一定会触发加载Object.class, String.class。此时不存在多线程的情况(`执行多个java命令那叫多进程,不是一个JVM`)。

URLClassLoader

   ExtClassLoader 是它的实现类。 最终的加载是委托给 ClassLoader#loadClass(String, boolean)。 它使用了一个同步块,同步块的对象锁锁的是 #getClassLoadingLock(String)。 使用了ConcurrentHashMap的一个特性: putIfAbsent。 因此,多线程环境中:putIfAbsent 保证了只有一个线程能往ConcurrentHashMap里面塞对象,且他们GET的对象一定是同一个。synchronized 保证了多线程环境下,只有一个类能够被加载。 之后的类加载都是获取加载好的类。

AppClassLoader

   AppClassLoader继承于URLClassLoader, 但是重写了ClassLoader#loadClass(String, boolean)。这个逻辑很简单,先找是否加载了这个类knownToNotExist(String), 方法是同步的。否则沿用ClassLoader#loadClass(String, boolean)逻辑。

如上, 至少在JDK原生的ClassLoader环境下, JDK通过synchronized/ConcurrentHashMap 等机制保证了各种环境下Class对象的唯一性。
至于BootStrapClassLoader, 没有翻源码。StringObject 肯定是独一份的。

图解和举例

普通Java应用

见类:Useless.java

import java.io.Serializable;public class Useless extends UselessParent {public Serializable s1 = new Serializable() {{System.out.println("域变量");}};public static Serializable s2 = new Serializable() {{System.out.println("静态域变量");}};static {System.out.println("静态代码块");}{System.out.println("代码块");}}

见类:UselessParent.java

import java.io.Serializable;public class UselessParent {public Serializable s1 = new Serializable() {{System.out.println(getClass() + "域变量");}};public static Serializable s2 = new Serializable() {{System.out.println(getClass() + "静态域变量");}};static {System.out.println("静态代码块");}{System.out.println(getClass() + "代码块");}}

和执行类:MainFather.java

public class MainFather {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,InstantiationException {Useless u;// u = new Useless();System.out.println(Useless.s2);System.out.println("-----------------------------------------");Class z;z =  Class.forName("com.baidu.qyp.job.clazz.Useless");System.out.println("-----------------------------------------");z = Class.forName("com.baidu.qyp.job.clazz.Useless", true, MainFather.class.getClassLoader());System.out.println("-----------------------------------------");z = Class.forName("com.baidu.qyp.job.clazz.Useless", false, MainFather.class.getClassLoader());System.out.println("-----------------------------------------");z = MainFather.class.getClassLoader().loadClass("com.baidu.qyp.job.clazz.Useless");System.out.println("-----------------------------------------");u = (Useless) z.newInstance();System.out.println("-----------------------------------------");u = new Useless();}
}

执行结果:

class UselessParent$2静态域变量
静态代码块
class Useless$2静态域变量
静态代码块
Useless$2@378bf509
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块

执行附图:

Created with Raphaël 2.2.0java MainFather初始化MainFatherUseless.s2触发Useless初始化先初始化UselessParent赋值UselessParent静态域执行UselessParent静态代码Useless同UselessParent打印输出Useless.s2余下四步皆只是加载类, 因为类已经被加载,因此无任何操作newInstance()和new效果一致实例化子类时优先实例化父类因为父类和子类已经初始化,不再初始化优先域变量,其次代码块程序结束

这也很好的解释了一个问题: 为什么静态元素和静态代码块在一个虚拟机里面只会执行一次:

  1. 默认习惯都是不会指定ClassLoader的,所属类也就只有一次初始化过程。
  2. 赋值静态域,或者执行静态代码块,是在类加载的流程中执行的。而这样的操作只会有一次。
  3. 赋值域,或者执行代码块,是在类实例化的流程中执行的,这样的操作根据程序需求可能有多次。

日常Web应用

这里的Web应用指的就是Tomcat Web应用。 其中的Tomcat启动模块跟普通Java应用并无区别。 附上一张Spring Web流程图。

Created with Raphaël 2.2.0java BootStrap start初始化BootStrap遇见类调用则初始化,遇见new或者newInstance()则实例化这些类的加载器都是AppClassLoaderTomcat使用反射的方式初始化了Tomcat_lib上述步骤指定了Loader的是URLClassLoaderTomcat指定了WEB-INF/class等位置由Tomcat加载使用的Loader为WebappClassLoaderBase这里全都是用户代码,或者引用的第三方jar包所有类实例化方式完全一致初始化子类时先初始化父类实例化子类时优先实例化父类因为父类和子类已经初始化,不再初始化优先域变量,其次代码块程序结束

参考文章:
http://www.cnblogs.com/ityouknow/p/5603287.html
http://www.cnblogs.com/ITtangtang/p/3978102.html

附录

UserClassLoaderAppClassLoaderExtClassLoaderBootStrap委托父加载器UserClassLoader 用户的Loader委托父加载器只关心java.* sun.*包委托父加载器UserClassLoaderAppClassLoaderExtClassLoaderBootStrap

  1. 双亲委托加载的序列图 ↩︎

图解Java类加载机制相关推荐

  1. 两道面试题,带你解析Java类加载机制

    2019独角兽企业重金招聘Python工程师标准>>> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa {static ...

  2. Java类加载机制详解【java面试题】

    Java类加载机制详解[java面试题] (1)问题分析: Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数 ...

  3. 谈谈 Java 类加载机制

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 来源:Rainstorm , github.com/c-rainstorm/blog/blob/m ...

  4. Java类加载机制:双亲委托模型

    Java类加载机制:双亲委托模型 前言(废话) 一如既往,这篇博客是我极为浅显的理解,仅仅是我记录我自己成长的一环而已.我以前听我老师说过,什么是进步,进步就是当你三个月后重新再看自己的代码,发现那就 ...

  5. 深入研究Java类加载机制

    深入研究Java类加载机制   类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. 研究类加载机制的第二个目的是让程序能动态的控制类加载 ...

  6. Java类加载机制深度分析

    为什么80%的码农都做不了架构师?>>>    Java类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. ...

  7. Java高级篇——深入浅出Java类加载机制

    转载自 Java高级篇--深入浅出Java类加载机制 类加载器 简单讲,类加载器ClassLoader的功能就是负责将class文件加载到jvm内存. 类加载器分类 从虚拟机层面讲分为两大类型的类加载 ...

  8. Java类加载机制的理解

    算上大学,尽管接触Java已经有4年时间并对基本的API算得上熟练应用,但是依旧觉得自己对于Java的特性依然是一知半解.要成为优秀的Java开发人员,需要深入了解Java平台的工作方式,其中类加载机 ...

  9. java 加载类java_深入研究Java类加载机制

    深入研究Java类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. 研究类加载机制的第二个目的是让程序能动态的控制类加载,比 ...

  10. java版如何使区块常加载,Java类加载机制 - suer27zhu的个人空间 - OSCHINA - 中文开源技术交流社区...

    首先上图 如图所示,Java类加载机制的六个阶段 Java代码编译完成后会生成对应的class文件,接着我们运行java命令的时候,其实是启动了JVM虚拟机执行class字节码文件的内容.大致分为六个 ...

最新文章

  1. python判断正数和负数教案_正数和负数 教学设计
  2. jquery on()方法和bind()方法的区别
  3. 谷歌为何会选用TypeScript?
  4. java代码怎样整体左移_java 多行代码左移
  5. DTMF采用RFC2833进行带内传输的实现[ZT]
  6. 斗拱展开面积表_144996_河南省仿古建筑工程计价综合单价2009
  7. 2020年最新程序员职业发展路线指南,超详细!
  8. 转: MATLAB: cat函数使用
  9. 使用canvas绘制等分圆
  10. 工厂如何引入ERP生产管理系统
  11. Vue学习之vue-cli脚手架下载安装及配置
  12. Q8四元数群的正规子群
  13. python爬房源信息_Python爬取链家二手房源信息
  14. 读书笔记 《拆掉思维里的墙》
  15. 计算机板块:外包引领软件业高成长
  16. Xshell6安装与使用
  17. JavaScript中的ReferenceError和TypeError两种错误的区别
  18. Flask、sqlite3、pipenv实现用户注册和登录(HandBook,菜鸟都会的)
  19. 基于容器制作镜像(apache)
  20. 解决Solaris应用程序开发内存泄漏问题 (1)

热门文章

  1. c语言实现费诺编码csdn,香农编码 哈夫曼编码 费诺编码的比较
  2. VLAN划分和网络配置实例
  3. FC SAN - 光纤通道存储区域网络
  4. 用Python做一个连连看游戏辅助脚本,完整编程思路分享
  5. Unity UGUI 流光特效
  6. 金蝶K3系统BOM数据批量审核/使用语句
  7. sql server 2000 sp3 补丁
  8. 联想拯救者 Y7000 Ubuntu 16.04无线网卡驱动安装踩坑教程
  9. 7万字总结Spring,这回能看懂Spring源码了!
  10. Chromium OS Autotest 客户端测试