文章目录

  • 1 简单介绍
  • 1.1 跨平台运行
  • 1.2 执行机制
  • 1.3 前端编译和后端编译
  • 1.4 分层编译
  • 1.4.1 解释模式
  • 1.4.2 编译模式
  • 1.4.3 提前编译器:jaotc
  • 2 类加载机制
  • 2.1 加载方式
  • 2.1.1 显式隐式加载
  • 2.1.2 Class.forName和ClassLoader区别
  • 2.2 加载过程
  • 2.2.1 加载
  • 2.2.2 链接
  • 2.2.2.1 验证
  • 2.2.2.2 准备
  • 2.2.2.3 解析
  • 2.2.3 初始化
  • 2.2.4 类加载总结
  • 2.3 类加载时机
  • 3 类加载器
  • 3.1 了解类加载器
  • 3.2 类加载器分类
  • 3.3 类加载机制
  • 3.3.1 类加载机制分类
  • 3.3.2 双亲委派机制
  • 3.3.2.1 双亲原理
  • 3.3.2.2 验证双亲原理
  • 3.3.2.3 双亲优点
  • 3.3.2.4 JVM在搜索类的时候,又是如何判定两个class是相同的
  • 3.3.3 深度分析Java的ClassLoader机制(源码级别)
  • 3.3.4 破坏双亲委派模型
  • 3.4 自定义类加载器
  • 3.5 其他加载器
  • 3.5.1 线程上下文类加载器
  • 3.5.2 类加载器与Web容器
  • 3.5.2.1 简介
  • 3.5.2.2 web容器解决的问题
  • 3.5.2.3 Tomcat 类加载机制
  • 3.5.2.4 tomcat打破双亲委派
  • 4 类加载器卸载机制
  • 4.1 classLoader的卸载机制
  • 4.2 类的生命周期和引用
  • 4.2.1 生命周期
  • 4.2.2 引用关系
  • 4.2.3 类的卸载


1 简单介绍

1.1 跨平台运行

Java的编译和平台独立性  
首先Java是平台独立性语言(C/C++就不是,java一次编译在各个平台上都能执行),这关键就在它的字节码JVM机制。Java程序编译后不是直接生成硬件平台的可执行代码,而是生成.class的字节码文件,再交由JVM翻译成对应硬件平台可执行的代码。(也就是说.java文件被javac指令编译为.class的字节码文件,再由JVM执行)。

1.2 执行机制

Java字节码的执行分为:即时编译解释执行,通常采用解释执行方式

  • 解释执行:是指解释器通过每次解释并执行一小段代码来完成.class程序的所有操作
    解释执行中有几种优化方式:
  • 栈顶缓存
    将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数站。这样便减少了寄存器和内存的交换开销。
  • 部分栈帧共享
    被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。
  • 执行机器指令
    在一些特殊情况下,JVM会执行机器指令以提高速度。
  • 即时编译:则是以方法为单位,将字节码.class文件一次性翻译为机器码后执行
    HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律(即:自适应优化执行),消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化
  • 静态提前编译Ahead Of TimeAOT编译)程序运行前,直接把Java源码文件(.java)编译成本地机器码的过程;
    优点: 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动; 把编译的本地机器码保存磁盘,不占用内存,并可多次使用;
    缺点:因为Java语言的动态性(如反射)带来了额外的复杂性,影响了静态编译代码的质量; 一般静态编译不如JIT编译的质量,这种方式用得比较少;

1.3 前端编译和后端编译

JVM的前端编译和后端编译是Java程序执行过程中的两个重要阶段:

  • 前端编译:这个阶段主要是将.java源文件编译成.class字节码文件。这个过程是由javac编译器完成的。javac编译器会对源代码进行词法分析语法分析语义分析生成字节码等步骤。
    它的优化更偏向于代码结构上的优化,它主要是为了提高程序员的编码效率,不怎么关注执行效率优化;例如,数据流和控制流分析、解语法糖等等。
  • 后端编译:这个阶段主要是将.class字节码文件转化为机器码 后端编译包括 即时编译[JIT]提前编译[AOT],区别于前端编译器,它们最终作用体现于运行期,致力于优化从字节码生成本地机器码的过程(它们优化的是代码的执行效率)。
    转化为机器码的过程是由JVMJust-In-Time (JIT) 编译器完成的。JIT编译器会在运行时将经常执行的字节码编译为机器码,这样可以提高程序的执行效率。JIT编译器还会进行一些优化,比如方法内联、死代码消除等,以进一步提高程序的执行效率。

1.4 分层编译

JVM 自己根据宿主机决定自己的运行模式,JVM 运行模式;[客户端模式-Client、服务端模式-Server],它们代表的是两个不同的即时编译器,C1(Client Compiler) C2 (Server Compiler)
分层编译分为:解释模式编译模式混合模式

  • 解释模式下运行时,编译器不介入工作;
  • 编译模式模式下运行,会使用即时编译器优化热点代码,有可选的即时编译器[C1 或 C2];
  • 混合模式为:解释模式和编译模式搭配使用。

1.4.1 解释模式

它不使用即时编译器进行后端优化

  • 强制虚拟机运行于 “解释模式” -Xint
  • 禁用后台编译 -XX:-BackgroundCompilation

1.4.2 编译模式

即时编译器会在运行时,对生成的本地机器码进行优化,其中重点关照热点代码。

-Xcomp  强制虚拟机运行于 "编译模式"

方法调用次数计数器阈值,它是基于计数器热点代码探测依据[Client模式=1500,Server模式=10000]
-XX:CompileThreshold=10

关闭方法调用次数热度衰减,使用方法调用计数的绝对值,它搭配上一配置项使用
-XX:-UseCounterDecay

除了热点方法,还有热点回边代码[循环],热点回边代码的阈值计算参考如下:
-XX:BackEdgeThreshold  = 方法计数器阈值[-XX:CompileThreshold] * OSR比率[-XX:OnStackReplacePercentage]

OSR比率默认值:Client模式=933,Server模式=140
-XX:OnStackReplacePercentag=100

所谓 即时,它是在运行过程中发生的,所以它的缺点也也明显:在运行期间需要耗费资源去做性能分析,也不太适合在运行期间去大刀阔斧的去做一些耗费资源的重负载优化操作。

1.4.3 提前编译器:jaotc

它是后端编译的另一个主角,它有两个发展路线,基于Graal [新时代的主角] 编译器开发

  • 与传统的 C、C++ 编译做的事情类似,在程序运行之前就把程序代码编译成机器码;好处是够快,不占用运行时系统资源,缺点是启动过程 会很缓慢;
  • 已知即时编译运行时做性能统计分析占用资源,那么,我们可以把其中一些耗费资源的编译工作,放到提前编译阶段来完成啊,最后在运行时即时编译器再去使用,那么可以大大节省即时编译的开销;这个分支可以把它看作是即时编译缓存;

但是它只支持 G1 或者 Parallel 垃圾收集器,且只存在JDK 9 以后的版本,暂不需要去关注它;JDK 9 以后的版本可以使用这个参数打印相关信息:-XX:PrintAOT

2 类加载机制

Java语言是一种具有动态性的解释性语言,类(Class)只有被加载到JVM中才能运行。
JVM会将编译生成的.class文件加载到内存中,并组织成为一个完整的Java程序。 这个加载过程则是由类加载器(ClassLoader和它的子类)来完成的,其实质是把类文件从硬盘读到内存中。

2.1 加载方式

2.1.1 显式隐式加载

Java中类的加载是动态的,它不会一次性加载所有类然后运行,而是先把保证程序能运行的基类先加载到JVM中,其他类则是在需要时再加载,这样就加快了加载速度,而且节约了程序运行过程中内存的开销

类的加载方式分为:

  • 隐式加载:程序使用new等方式创建对象,会隐式的调用类加载器。
  • 显式加载:直接调用class.forName()方法

2.1.2 Class.forName和ClassLoader区别

Class.forName()ClassLoader 都可以对类进行加载。ClassLoader 就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是通过一个类的全限定名来获取描述此类的二进制字节流,获取到二进制流后放到JVM中。
Class.forName() 方法实际上也是调用的 ClassLoader 来实现的。
Class.forName(String className);这个方法的源码是

@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
 Class<?> caller = Reflection.getCallerClass();
 return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

最后调用的方法是 forName()这个方法,在这个forName()方法中的第二个参数被默认设置为了true,这个参数代表是否对加载的类进行初始化,设置为true时会类进行初始化,代表会执行类中的静态代码块,以及对静态变量的赋值等操作。
也可以调用Class.forName(String name, boolean initialize,ClassLoader loader);方法来手动选择在加载类的时候是否要对类进行初始化。

二者区别:

  • Class.forName():这是一个静态方法,主要用于加载类。当知道类的全路径名时,可以使用这个方法来加载类。这个方法不仅仅是将类的.class文件加载到jvm中,它还会对类进行解释,执行类中的静态代码块。如果类已经被加载,则返回这个类的Class对象。
  • ClassLoader:这是一个抽象类,它的子类主要用于从系统外部获取二进制数据流,这些二进制数据流对应的是特定的类,接着将这些类的二进制数据流加载到 jvm 内部,生成对应的Class对象。ClassLoader只进行类的加载,不执行类中的任何代码。
    Java中,静态代码块静态变量初始化是在类加载的时候执行的,而类加载的时机是在第一次使用这个类的时候。所以,当使用ClassLoader去加载一个类的时候,只是将这个类加载到了JVM中,并没有真正去使用这个类,所以静态代码块或静态变量初始化并没有被执行。
    但是,真正去使用这个类的时候,比如创建这个类的实例,或者调用这个类的静态方法,那么这个类就会被初始化,静态代码块和静态变量初始化就会被执行。
    所以,ClassLoader在加载类时不执行静态代码,但是在执行时会执行静态代码
  • Class.forName() 除了会将类的.class文件加载到jvm中之外,还会对类进行初始化,执行类中的静态代码块;而ClassLoader只负责加载类,不执行类中的任何代码。

2.2 加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载连接初始化 3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化

java编译时依赖 java编译依赖class文件_Classloader

2.2.1 加载

加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

  • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
  • JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

2.2.2 链接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段:验证准备解析

2.2.2.1 验证

验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全

验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证元数据验证字节码验证符号引用验证

验证需要四步验证:
class文件校验器需要四趟独立扫描来完成验证工作,其中:
第一趟扫描在装载时进行,会对class文件进行结构检查,如
(1) 对魔数进行检查,以判断该文件是否是一个正常的class文件
(2) 对主次版本号进行检查,以判断class文件是否与java虚拟机兼容
(3) 对class文件的长度和类型进行检查,避免class文件部分缺失或被附加内容
第二趟扫描在连接过程中进行,会对类型数据进行语义检查,主要检查各个类的二进制兼容性(主要是查看超类和子类的关系)和类本身是否符合特定的语义条件
(1) final类不能拥有子类
(2) final方法不能被重写(覆盖)
(3) 子类和超类之间没有不兼容的方法声明
(4) 检查常量池入口类型是否一致(如CONSTANT_Class常量池的内容是否指向一个CONSTANT_Utf8字符串常量池)
(5) 检查常量池的所有特殊字符串,以确定它们是否是其所属类型的实例,以及是否符合特定的上下文无关语法、格式
第三趟扫描为字节码验证,其验证内容和实现较为复杂,主要检验字节码是否可以被java虚拟机安全地执行,分析数据流和控制,确定语义是合法的,符合逻辑的,主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现
第四趟扫描在解析过程中进行,为符号引用验证。在动态连接过程中,通过保存在常量池的符号引用查找被引用的类、接口、字段、方法时,在把符号引用替换成直接引用时,首先需要确认查找的元素真正存在,然后需要检查访问权限、查找的元素是否是静态类成员而非实例成员,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

2.2.2.2 准备

类准备阶段负责为类的静态变量分配内存,并设置默认初始值

2.2.2.3 解析

将类的二进制数据中的符号引用替换成直接引用
Java之所以是符号引用而不是像c语言那样,编译时直接指定其他类型,是因为java动态绑定的,只有在运行时根据某些规则才能确定具体依赖的类型实例,这正是java实现多态的基础

说明一下符号引用和直接引用区别:

  • 符号引用:是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关
  • 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

2.2.3 初始化

初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的

如果类中有语句:private static int a = 10,它的执行过程是这样的:
首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量astatic的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10

2.2.4 类加载总结

类的加载主要分为3步:

  • 装载:根据查找路径找到相应的class文件,然后倒入。
  • 链接
  1. 检查:检查待记载的class文件的正确性。
  2. 准备:给类中的静态变量分配存储空间。(这里用到了static关键字的知识)
  3. 解析:将符号引用转换成直接引用(此步是可选的)
  • 初始化:对静态变量和静态代码块执行初始化工作。这个阶段才是真正开始执行类中的字节码

2.3 类加载时机

3 类加载器

当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的

3.1 了解类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

3.2 类加载器分类

类加载器的图示:

java编译时依赖 java编译依赖class文件_类加载器_02


JVM预定义有三种类加载器,当一个JVM启动的时候,Java开始使用如下三种类加载器:

  • 根类加载器(bootstrap classloader):它用来加载 Java 的核心类,是用原生代码来实现的,并且不继承自java.lang.ClassLoader(负责加载$JAVA_HOMEjre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
    由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
    通过查找sun.boot.class.path这个系统属性即可得知
    System.out.println(System.getProperty("sun.boot.class.path"));
  • 扩展类加载器(extensions classloader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null 假如当我们使用根加载器加载的对象使用此方法获取到的ClassLoadernull,为什么是这样呢?前面已经说了,根类加载器是使用C++编写的,JVM不能够也不允许程序员获取该类,所以返回的是null,还有一点,如果此对象表示的是一个基本类型或void,则返回null,其实进一步的含义就是:Java中所有的基本数据类型都是由根加载器加载的
  • 系统类加载器(app classloader):被称为系统(也称为 应用 )类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类类加载器:这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoaderExtension ClassLoaderApp ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoaderApp ClassLoader类加载器

3.3 类加载机制

3.3.1 类加载机制分类

JVM的类加载机制主要有如下3种:

  • 全盘负责:是指当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
    通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才给子类去加载
  • 缓存机制:会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

或者这样类加载器三个机制:委托单一性可见性

  • 委托:指加载一个类的请求交给父类加载器,若父类加载器不可以找到或者加载到,再加载这个类
  • 单一性:指子类加载器不会再次加载父类加载器已经加载过的类
  • 可见性:子类加载器可以看见父类加载器加载的所有类,而父类加载器不可以看到子类加载器加载的类

3.3.2 双亲委派机制

3.3.2.1 双亲原理

java编译时依赖 java编译依赖class文件_java编译时依赖_03


双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

注意一点:加载器之间不是继承关系,而是组合关系

3.3.2.2 验证双亲原理
  1. 验证双亲原理一:
ClassLoader loader = MapDemo.class.getClassLoader();//获得加载ClassLoaderTest.class这个类的类加载器 
        while(loader != null) {
        System.out.println(loader);
        loader = loader.getParent(); //获得父类加载器的引用 
        }
        System.out.println(loader);
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@27716f4
null
运行结果分析:
第一行结果说明:MapDemo的类加载器是AppClassLoader。
第二行结果说明:AppClassLoader的类加载器是ExtClassLoader,即parent=ExtClassLoader。
第三行结果说明:ExtClassLoader的类加器是Bootstrap ClassLoader,
因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因
  1. 验证双亲原理二:
    MapDemo.class打包成MapDemo.jar,放到Extension ClassLoader的加载目录下(JAVA_HOME/jre/lib/ext),然后重新运行这个程序,得到的结果会是什么样呢
运行结果:
sun.misc.Launcher$ExtClassLoader@27716f4
null

运行结果分析:
为什么第一行的结果是ExtClassLoader呢?
因为ClassLoader的委托模型机制,当我们要用MapDemo.class这个类的时候,AppClassLoader在试图加载之前,先委托给Bootstrcp ClassLoaderBootstracp ClassLoader发现自己没找到,它就告诉ExtClassLoader,兄弟,我这里没有这个类,你去加载看看,然后Extension ClassLoader拿着这个类去它指定的类路径(JAVA_HOME/jre/lib/ext)试图加载,唉,它发现在MapDemo.jar这样一个文件中包含MapDemo.class这样的一个文件,然后它把找到的这个类加载到内存当中,并生成这个类的Class实例对象,最后把这个实例返回。所以MapDemo.class的类加载器是ExtClassLoader
第二行的结果为null,是因为ExtClassLoader的父类加载器是Bootstrap ClassLoader

  1. 验证双亲原理三:
    Bootstrcp ClassLoader来加载MapDemo.class,有两种方式:
    方式一:在jvm中添加-Xbootclasspath参数,指定Bootstrcp ClassLoader加载类的路径,并追加我们自已的jar(MapDemo.jar)

    方式二:将class文件放到JAVA_HOME/jre/classes/目录下
    MapDemo.jar解压后,放到JAVA_HOME/jre/classes目录下,如下图所示:
    提示:jre目录下默认没有classes目录,需要自己手动创建一个
3.3.2.3 双亲优点

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要让子ClassLoader再加载一次

其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

3.3.2.4 JVM在搜索类的时候,又是如何判定两个class是相同的

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Javaorg.classloader.simple.NetClassLoaderSimplejavac编译之后生成字节码文件NetClassLoaderSimple.classClassLoaderAClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型

3.3.3 深度分析Java的ClassLoader机制(源码级别)

class loader是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,class loader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。
接下来我们看loadClass方法的实现方式:

protected Class> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    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
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    // 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;
        }
    }

使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类,如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)
接下来,我们开始分析该方法。
protected Class> loadClass(String name, boolean resolve) 该方法的访问控制符是protected,也就是说该方法 同包内和派生类中可用 返回值类型Class

首先,在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对象。

Class c = findLoadedClass(name); 在这里,在加载类之前先调用findLoadedClass方法检查该类是否已经被加载过,findLoadedClass会返回一个Class类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve的值来决定是否处理该对象)。 如果,该类没有被加载过,那么执行以下的加载过程

try {
    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
}

3.3.4 破坏双亲委派模型

由于双亲委派模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java的世界中大部分的类加载器都遵循者模型,但也有例外,到目前为止,双亲委派模型有过3次大规模的 “被破坏” 的情况。

  • 第一次:在双亲委派模型出现之前-----即JDK1.2发布之前。
  • 第二次:是这个模型自身的缺陷导致的。 我们说过,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为基础,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?
    这不是没有可能的。一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能 “认识“ 这些代码啊。因为这些类不在 rt.jar 中,但是启动类加载器又需要加载。
    怎么办呢?
    为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认使用应用程序类加载器。
    有了线程上下文加载器,JNDI 服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打破了双亲委派模型的层次结构,来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。但这无可奈何,Java中所有涉及SPI的加载动作基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
  • 第三次:为了实现热插拔热部署模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
    Java 程序中基本有一个共识:OSGI 对类加载器的使用是值得学习的,弄懂了OSGI的实现,就可以算是掌握了类加载器的精髓。

3.4 自定义类加载器

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jarclass,如果我们想加载其它位置的类或jar时,比如:要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader
定义自已的类加载器分为两步:

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

那么父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法
示例:自定义一个NetworkClassLoader,用于加载网络上的class文件

package classloader; 
   
import java.io.ByteArrayOutputStream; 
import java.io.InputStream; 
import java.net.URL; 
   
/**
 * 加载网络class的ClassLoader
 */ 
public class NetworkClassLoader extends ClassLoader { 
    private String rootUrl; 
   public NetworkClassLoader(String rootUrl) { 
        this.rootUrl = rootUrl; 
    } 
   @Override 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
        Class clazz = null;//this.findLoadedClass(name); // 父类已加载     
            byte[] classData = getClassData(name);  //根据类的二进制名称,获得该class文件的字节码数组 
            if (classData == null) { 
                throw new ClassNotFoundException(); 
            } 
            clazz = defineClass(name, classData, 0, classData.length);  //将class的字节码数组转换成Class类的实例 
     
        return clazz; 
    } 
   
    private byte[] getClassData(String name) { 
        InputStream is = null; 
        try { 
            String path = classNameToPath(name); 
            URL url = new URL(path); 
            byte[] buff = new byte[1024*4]; 
            int len = -1; 
            is = url.openStream(); 
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
            while((len = is.read(buff)) != -1) { 
                baos.write(buff,0,len); 
            } 
            return baos.toByteArray(); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } finally { 
            if (is != null) { 
               try { 
                  is.close(); 
               } catch(IOException e) { 
                  e.printStackTrace(); 
               } 
            } 
        } 
        return null; 
    }    
    private String classNameToPath(String name) { 
        return rootUrl + "/" + name.replace(".", "/") + ".class"; 
    } 
   }

3.5 其他加载器

3.5.1 线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源

前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPIJDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPIApache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到

3.5.2 类加载器与Web容器

3.5.2.1 简介

对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是 首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:

  • 每个 Web 应用自己的 Java 类文件和其使用的库的 jar 包,分别放在 WEB-INF/classesWEB-INF/lib目录下面。
  • 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
  • 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确
3.5.2.2 web容器解决的问题

Tomcat是个web容器, 那么它要解决什么问题:

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
    Tomcat 如果使用默认的类加载机制行不行?
    答案是不行的。如果使用默认的类加载器机制,那么是 无法加载两个相同类库的不同版本的,默认的类加载器是不管什么版本的,只在乎的全限定类名,并且只有一份。
  • 部署在同一个web容器中相同的类库相同的版本可以共享。
  • web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  • web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,所以,web容器需要支持 jsp 修改后不用重启。
    我们想我们要怎么实现 jsp 文件的热修改,jsp 文件其实也就是 class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的 jsp 是不会重新加载的。我们可以直接卸载掉这 jsp 文件的类加载器,所以每个 jsp 文件对应一个唯一的类加载器,当一个jsp 文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
3.5.2.3 Tomcat 类加载机制

java编译时依赖 java编译依赖class文件_类加载器_04


我们看到,前面3个类加载和默认的一致,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*/server/*/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

  • commonLoaderTomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoaderTomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoaderSharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoaderShared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

3.5.2.4 tomcat打破双亲委派

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader 加载自己的目录下的class文件,不会传递给父类加载器。

我们扩展出一个问题:如果 tomcatCommonClassLoader 想加载 WebAppClassLoader 中的类,该怎么办?可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作

4 类加载器卸载机制

4.1 classLoader的卸载机制

jvm中没有提供classclassloaderunload方法,那热部署及osgi中是通过什么机制来实现的呢?实现思路主要是通过更换classLoader进行重新加载。之前的classloader及加载的class类在没有实例引用的情况下,在perm区gc的情况下会被回收掉.

perm区gc时回收掉没有引用的class是一个怎样的过程呢?
perm区达到回收条件后,对class进行引用计算,对于没有引用的class进行回收

如果有实例类有对classloader的引用,perm区class将无法卸载,导致perm区内存一直增加,进而导致perm space error

public static Map pool = new HashMap();
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
        for (int i=0;i<10000000;i++){
            test(args);
        }
    }

public static void test(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        ClassLoader cl = new MyLoader(Main.class.getClassLoader());
        String className = "RealPerson";

        @SuppressWarnings("unchecked")
        Class<Person> clazz = (Class<Person>) cl.loadClass(className);
        Person p = clazz.newInstance();
        p.setName("qiang");
        pool.put(System.nanoTime(), p);
        cl = p.getClass().getClassLoader();
    }

推测:
osgi的bundle进行热部署时有个条件:export class 必须是兼容的.否则需要重启整个应用才会生效,为什么呢?
osgiexport class是被bundleparent classloader加载的,bundle内部其他类是bundleclassloader加载的,bundle更换后,重新创建classloader,并对bundle进行加载,之前的加载靠jmv gc回收掉.
那osgi中explort class如果有实例引用的话,是否会导致class无法被gc掉?
如果osgi中没有做过处理,应该会出现此问题

4.2 类的生命周期和引用

4.2.1 生命周期

当Sample类被加载、连接和初始化后,它的生命周期就开始了。
当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

4.2.2 引用关系

加载器和Class对象:
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。
另一方面,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()方法,就能获得它的类加载器。
由此可见,Class实例和加载它的加载器之间为双向关联关系。

类、类的Class对象、类的实例对象:
一个类的实例总是引用代表这个类的Class对象。
Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。
此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象

4.2.3 类的卸载

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。
Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

由用户自定义的类加载器加载的类是可以被卸载的。

java编译时依赖 java编译依赖class文件_java编译时依赖_05

loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。

如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)