八、类加载机制与类加载

一、类的生命周期

  • 类的加载

        当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接(验证,准备,解析)、初始化三步来实现对这个类进行初始化。

  • 类的生命周期 7 个阶段

        类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。

其中验证、准备、解析 3 个部分统称为连接(Linking)

  • 阶段顺序

        加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,但是对于“解析”阶段则不一定,它在某些情况下可以在初始化之后再开始,这样做是为了支持 java 的运行时绑定特征(也称为动态绑定或晚期绑定)。

二、加载

        加载 就是指将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。

  • 加载的时机

        什么时候开始类第一个阶段“加载”,虚拟机规范没有强制约束,这点交给虚拟机的具体实现来自由把控。“加载 loading”阶段是整个类加载(class loading)过程的第一个阶段。

  • 加载阶段虚拟机需要完成以下 3 件事情:
  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。(类加载的最终产品就是一个class对象)

注意:比如“通过一个类的全限定名来获取定义此类的二进制字节流”没有指定一定得从某个 class 文件中获取,所以我们可以从 zip 压缩包、从网络中获取、运行时计算生成、数据库中读取、或者从加密文件中获取等等。

        我们也可以通过工具 JHSDB 可以看到,JVM 启动后,相关的类已经加载进入了方法区,成为了方法区的运行时结构。

package cn.ex2; public class JVMObject { }

1、 Attach 上 JVM 启动的进程

2、 打开 Class Browser

3、 可以看到很多 class 已经被加载进来了

4、 找到 JVMObject,注意!这里已经是内存了,所以说相关的类已经加载进入了方法区,成为了方法区的运行时结构。

三、验证

        验证 就是验证是否有正确的内部结构,并和其他类协调一致

        验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

        但从整体上看,验证阶段大致上会完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

阶段一:文件格式验证(非重点)

        第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:

  • 是否以魔数 OxCAFEBABE 开头。
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT Utf8 info 型的常量中是否有不符合 UTF-8 编码的数据。
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

以上的部分还只是一小部分,没必要进行深入的研究。

总结:

        这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入 Java 虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构(内存)上进行的,不会再直接读取、操作字节流了。

阶段二:元数据验证(非重点)

        第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都-致,但返回值类型却不同等)。

以上的部分还只是一小部分,没必要进行深入的研究。

总结:

        元数据验证是验证的第二阶段,主要目的是对类的元数据信息进行语义校验,保证不存在与《Java 语言规范》定义相悖的元数据信息。

阶段三:字节码验证(非重点)

        第三阶段字节码验证是整个验证过程中最复杂的一个阶段, 主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,本阶段就要对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中”这样的情况。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的,例如:可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

以上的部分还只是一小部分,没必要进行深入的研究。

总结:

        如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的。

阶段四:符号引用验证(非重点)

        最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段一解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的可访问性( private、 protected. public、 )
  • 是否可被当前类访问。

总结:

        符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,将会抛出异常。

验证(总结)

        验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、 但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

-Xverify:none 参数来关闭大部分的类验证措施

四、准备

        准备 负责为类的静态成员分配内存,并设置默认初始化值

        准备阶段是正式为类中定义的变量(被 static 修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这个阶段中有两个容易产生混淆的概念需要强调一下:

        首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

        那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 是后续的初始化环节。

基本数据类型的零值表

五、解析

        将类的二进制数据中的符号引用替换为直接引用

        解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。

        符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。

        直接引用的对象都存在于内存中,你可以把通讯录里的女友手机号码类比为符号引用,把面对面和你吃饭的女朋友,类比为直接引用。

解析大体可以分为:

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

我们了解几个经常发生的异常,就与这个阶段有关。

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)

六、初始化

        进行类的初始化,不是new对象

        是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。在准备阶段,静态变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化主要是对一个 class 中的 static{}语句进行操作(对应字节码就是 clinit 方法)。初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量(静态变量)的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

        初始化阶段,虚拟机规范则是严格规定了有且只有 6 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的Java 代码场景是:

  • 使用 new 关键字实例化对象的时候。
  • 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
  • 调用一个类的静态方法的时候。

2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

6)当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

Class的初始化时机总结:

  • 创建Class的实例
  • 访问Class的静态变量,或者为静态变量赋值
  • 调用Class的静态方法
  • 使用反射方式来强制创建某个Class或接口对应的java.lang.Class对象
  • 初始化某个Class的子类
  • 直接使用java.exe命令来运行某个主类

初始化的单例模式(线程安全)

        虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点。

        static初始化是线程安全的吗?是的

七、初始化案例分析

案例代码

 /**
 * 初始化的各种场景
 * 通过VM参数可以观察操作是否会导致子类的加载 -XX:+TraceClassLoading
 **/
public class Initialization {
        public static void main(String[]args){
            Initialization initialization = new Initialization();
            initialization.M1();//打印子类的静态字段
    //        initialization.M2();//使用数组的方式创建
    //        initialization.M3();//打印一个常量
    //        initialization.M4();//如果使用常量去引用另外一个常量
        }
        public void M1(){
            // 如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化(但是子类会被加载)
            System.out.println(SubClaszz.value);
        }
        public void M2(){
            //使用数组的方式, 不会触发初始化(触发父类加载,不会触发子类加载)
            SuperClazz[]sca = new SuperClazz[10];
        }
        public void M3(){
            //打印一个常量,不会触发初始化(同样不会触类加载、编译的时候这个常量已经进入了自己class的常量池)
            System.out.println(SuperClazz.HELLOWORLD);
        }
        public void M4(){
            //如果使用常量去引用另外一个常量(会不会初始化SuperClazz  1  不会走2)
            System.out.println(SuperClazz.WHAT);
        }
}

/**
 * 父类
 */
public class SuperClazz {
        static     {
            System.out.println("SuperClass init!");
        }
        public static int value = 123;
        public static final String HELLOWORLD="hello king";
        public static final int WHAT = value;
}

/**
 * 子类
 */
public class SubClaszz extends SuperClazz {
   static{
      System.out.println("SubClass init!");
   }

}

 案例1:

运行结果

——结果解析

如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化,但是子类会被加载

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化

使用-XX:+TraceClassLoading观察操作是否加载子类

案例 2

——代码

运行结果

——结果解析

使用数组的方式, 不会触发初始化,只触发父类加载,不会触发子类加载

案例3

运行结果

——结果解析

不会触发子类及父类的加载和初始化

为什么不会触发类加载,因为在编译的时候,常量数据已经进入自己类的常量池

案例 4

运行结果

——结果解析

如果使用常量去引用另外一个常量,这个值编译时无法确定,所以必须要触发初始化。

八、类加载器

        整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤的事(加载、验证、准备、解析、初始化)。负责将.class文件加载到内存中,并为之生成对应的Class对象。虽然我们不需要关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行。

用途:热加载、代码保护和加解密、类层次划分、OSGi等

自定义类加载对类进行加密和解密。

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:

  • 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++语言实现,是虚拟机自身的一部分;
  • 一种就是所有其他的类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

JDK提供的三层类加载器

类加载器的组成

  • Bootstrap ClassLoader 根类加载器
  • Extension ClassLoader 扩展类加载器
  • System ClassLoader 系统类加载器

九、类加载器的作用

1.启动类加载器-Bootstrap ClassLoader

        这是加载器中的扛把子,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是jre/lib目录下的 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。这个加载器是 C++ 编写的,随着 JVM 启动。

        这个类加载器负责将存放在<JAVA_HOME>/jre/lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用 null 代替即可。

2.扩展类加载器-Extention ClassLoader

        扩展类加载器,主要用于加载JRE的扩展目录(jre/lib/ext 目录下) 中的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。这个加载器是个 Java 类,继承自 URLClassLoader。

        这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载<JAVA_HOME>/jre/lib/ext 目录中的,或者被java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3.应用程序类加载器Application ClassLoader( System ClassLoader)

        这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。负责在JVM启动时加载来自 java 命令的class文件,以及classpath环境变量所指定的jar包和类路径。

        这个类加载器由 sun.misc.Launcher $AppClassLoader 实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。我们的应用程序都是由这 3 种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

自定义类加载器 Customr ClassLoader

自定义加载器,支持一些个性化的扩展功能。

类加载中的问题

        如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。

        对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

        这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

class加解密案例(deencrpt 代码)

通过位的二进制异或运算进行加解密(一次就是加密,再运算一次就是解密)

1.DemoUser.class 重命名为 DemoUserSrc.class 同时删掉 DemoUser.class,再通过 XorEncrpt 加密生成 DemoUser.class,使用编辑工具查看下加密前和加密后

2.写一个自定义的类加载器,继承 ClassLoader,同时在加载时进行解密。

3.写一个 DemoRun 类,使用自义定的类加载器加密,再打印类的对象,看它是哪个类加载器加载的,是否能正常显示。

加解密的项目中运用:可以使用把代码使用私钥加密,在解析阶段使用公钥解密。这样跟用户做项目时提供对应的公钥,自己提供私钥加密后的代码信息。在类加载时使用公钥解密运行。这样可以可以确保源代码的保密性。

十、双亲委派机制

        对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性

        双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

        这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

        简单点讲,双亲委派模型是每次加载一个类的时候都向上询问是否加载过,如果没有,上级能否加载,不能再往下一级一级退。

  • 双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

  • 双亲委派模型及其好处
    • 沙箱安全机制:防止核心API库被随意篡改
    • 保证被加载类的唯一性:在这种委派机制下,同一应用中的 class 文件永远只会加载一次。
    • 稳定:Java 类随着它的类加载器一起具备了带有优先级的层次关系,保证 java 程序稳定运行。

        使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

  • 类加载的过程

  • 先判断当前加载器是否已加载目标类,如已加载直接返回,如未加载则委托父加载器加载;
  • 父类加载器拿到目标类后先判断自己是否已加载目标类,如已加载则返回该类,如未加载再委托自己的父加载器加载。如此递归循环直到当前类加载器没有父加载器(即当前加载器为引导类加载器),这时交由引导类加载器去加载;
  • 引导类加载器加载目标类时在自己的类路径下找寻目标类文件,如找到了则直接加载该类,如未找到则向下委托子类加载器去加载。直到找到该类文件且成功加载为止

        我们可以翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。

ClassLoader.java

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1、使用锁所以是线程安全的
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 2、判断当前类加载器是否已加载目标类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) { 
                   // 3、当前类加载器有父类加载器,将目标类委托给父类加载器加载(注意:这里其实是递归)
                    c = parent.loadClass(name, false);
                } else {
                    // 4、无父类加载器则委托给引导类加载器加载,(只有引导类没有父类加载器)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 5、父类加载器不给加载,则自己加载
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 6、自己加载
                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) {
            // 7、调用URLClassLoader 的 findClass 方法在当前类加载器的类路径中寻找目标类文件,
            resolveClass(c);
        }
        return c;
    }
}

十一、自定义类加载器

ClassLoader 中的 loadClass 方法中的代码逻辑就是双亲委派模型:

        在自定义 ClassLoader 的子类时候,我们常见的会有两种做法,一种是重写 loadClass 方法,另一种是重写 findClass 方法。

        其实这两种方法本质上差不多,毕竟 loadClass 也会调用 findClass,但是从逻辑上讲我们最好不要直接修改 loadClass 的内部逻辑。建议的做法是只在 findClass 里重写自定义类的加载方法。

        loadClass 这个方法是实现双亲委托模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委托模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写 loadClass 方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。

        自定义类加载器继承 ClassLoader 类不覆盖 loadClass 方法,只重写 findClass 方法仍然满足双亲委派模型

        由于只重写了 findClass 方法并没有重写 loadClass 方法,故没有改变父类委托机制。

        也就数说如果某个.class可以被父类加载,我们自定义的类加载器就不会被执行了。比如 Animal.java 被自动编译为 Animal.class 放在 bin 目录下,AppClassLoader 完全可以加载,所以就不调用自定义的加载器了。重写 findClass方法的自定义类,首先会通过父类加载器进行加载,如果所有父类加载器都无法加载,再通过用户自定义的 findClass 方法进行加载。如果父类加载器可以加载这个类或者当前类已经存在于某个父类的容器中了,这个类是不会再次被加载的,此时用户自定义的 findClass 方法就不会被执行了。

如果我们自定义类加载器时候覆盖 loadClass 方法,不再去找父类加载器( c = parent.loadClass(name, false);)就打破了这这个双亲委派。

双亲委派模型

  • 继承ClassLoader,重新findClass方法
public class KingClassLoader extends ClassLoader {

    private String path;

    public KingClassLoader(String path){
        this.path = path;
    }

    // 用于寻找类文件
    @Override
    public Class findClass(String name){
        System.out.println("自定义findClass");
        byte[] b = loadClassData(name);
        return defineClass(name,b,0,b.length);
    }

    public byte[] loadClassData(String name) {
        name = path + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;

        try {
            in = new FileInputStream(new File(name));
            out = new ByteArrayOutputStream();
            int i = 0;
            while ((i = in.read()) != -1){
                out.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                out.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return out.toByteArray();
    }
}
public class ClassLoaderCheck {
    public static void main(String[] args) throws Exception {
        KingClassLoader classloader = new KingClassLoader("/Users/myspace/");
        Class c = classloader.loadClass("Test");
        System.out.println(c.getClassLoader());
        System.out.println(c.getClassLoader().getParent());
        System.out.println(c.getClassLoader().getParent().getParent());
        c.newInstance();
    }
}
//输出
自定义findClass
cn.others.KingClassLoader@6a38e57f
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@67f89fa3
  •  继承ClassLoader,重写loadClass方法
public class MyClassLoader extends ClassLoader {

    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
    /**
     * 重写类加载方法,实现自己的类加载逻辑,打破双亲委派。就是不先问parent
     * @param name
     * @param resolve
     * @return
     */
    @Override
    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) {
                // 指定自定义类的加载方式,其他类走双亲委派机制
                if (!name.startsWith("cn.others")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}
public class ClassLoaderCheck {
    public static void main(String[] args) throws Exception {
        MyClassLoader classloader = new MyClassLoader("/Users/guxl/myspace/project/p11_jvm/target/classes/");
        Class c = classloader.loadClass("cn.guxl.others.Test2");
        System.out.println(c.getClassLoader());
        System.out.println(c.getClassLoader().getParent());
        System.out.println(c.getClassLoader().getParent().getParent());
        c.newInstance();
    }
}
//输出
cn.guxl.others.MyClassLoader@6a38e57f
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3abfe836

十二、Tomcat类加载机制

Tomcat类加载器层次结构

        Tomcat 本身也是一个 java 项目,因此其也需要被 JDK 的类加载机制加载,也就必然存在引导类加载器、扩展类加载器和应用(系统)类加载器。

        CommonClassLoader 作为 CatalinaClassLoader 和 SharedClassLoader 的 parent,而 SharedClassLoader 又可能存在多个 children 类加载器 WebAppClassLoader,

        一个 WebAppClassLoader 实际上就对应一个 Web 应用,那 Web 应用就有可能存在 Jsp 页面,这些 Jsp 页面最终会转成 class 类被加载,因此也需要一个 Jsp的类加载器。

        需要注意的是,在代码层面 CatalinaClassLoader、SharedClassLoader、CommonClassLoader 对应的实体类实际上都是 URLClassLoader 或者 SecureClassLoader,一般我们只是根据加载内容的不同和加载父子顺序的关系,在逻辑上划分为这三个类加载器;而 WebAppClassLoader 和 JasperLoader 都是存在对应的类加载器类的。

当 tomcat 启动时,会创建几种类加载器:

1、Bootstrap 引导类加载器 加载 JVM 启动所需的类,以及标准扩展类(位于 jre/lib/ext 下)

2、System 系统类加载器 加载 tomcat 启动的类,比如 bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下。

3、CommonClassLoader 通用类加载器加载 tomcat 使用以及应用通用的一些类,位于 CATALINA_HOME/lib 下,比如 servlet-api.jar。CatalinaClassLader 实现了 Tomcat 类和 web 应用类的隔离,如果二者之间需要共享一些类怎么办?这里就需要 CommonClassLoad,它所加载的所有类都可以被 SharedClassLoader 和 CatalinaClassLoader 使用,从而实现web应用和tomcat对一些类的共享。

4、CatalinaClassloader:该类加载器专门加载Tomcat自身的类,从而和web应用的类做一个隔离

5、SharedClassLoader:该类加载器存在是为了解决不同Web应用之间共享类库,并且不会重复加载相同的类。它作为 WebAppClassLoader 的父加载器,专门加载Web应用之间的共享类。

6 、webappClassloader:该类加载器每个应用在部署后,都会创建一个唯一的类加载器实例。该类加载器会加载位于 WEB-INF/lib 下的 jar 文件中的 class 和 WEB-INF/classes下的 class 文件。

tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。

tomcat类加载过程

        对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader (WebApp类加载器)的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔离不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。

那么 tomcat 是怎么打破双亲委派机制的呢?

        可以看图中的 WebAppClassLoader,WebAppClassLoader 负责加载  webapp 目录的jar 和 class。它加载自己目录下的.class 文件,并不会传递给父类的加载器。即没有遵循委外双亲模型。 

WebappClassLoader extends WebappClassLoaderBase
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);

            // (0) Check our previously loaded local class cache
            // 检查缓存1,Class是否已经被当前ClassLoader装载过
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }

            // (0.1) Check our previously loaded class cache
            // 检查缓存2,Class是否已经被当前 parent-ClassLoader装载过
            clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
            
            // ....
            // ....
            // ....
            
            // 参数控制是否可以双亲委派 , delegate = false
            boolean delegateLoad = delegate || filter(name, true);

            // (1) Delegate to our parent if requested
            // 如果有要求可以委托给父类加载器
            if (delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader1 " + parent);
                try {
                    // 委托给parent
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
            
            // (2) Search local repositories
            // 当前classs load 加载
            if (log.isDebugEnabled())
                log.debug("  Searching local repositories");
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // (3) Delegate to parent unconditionally
            // 无条件由父类加载加载
            if (!delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader at end: " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
}

但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。

Tomcat怎么样保证类的隔离性?

WebAppClassLoader  没有遵循双亲委托模式,查看WebAppClassLoader源码,它的具体加载逻辑如下:

第1步:检查当前classloader 的缓存 resourceEntries,是否已经加载,如果有直接返回。

第2步:检查 jvm 自带加载器是否加载过。

第3步:尝试用  javaseClassLoader 去加载,为什么要做这个尝试呢,防止代码重写了java.lang.String  等jdk核心类,从而导致异常。

第4步:如果设置了双亲委托模式 delegateLoad=true ,尝试用父加载器加载。

第5步:尝试从当前 classloader 制定的目录中加载,同时会将加载信息放到缓存中 resourceEntries

这样就实现了资源隔离。

补充Spring的加载问题

        在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring 是通过调用 Class.forName 来加载业务类的。

                我在前面提到,Web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免重复加载。Spring 作为共享的第三方 JAR 包,它本身是由 SharedClassLoader 来加载的,Spring 又要去加载业务类,按照前面那条规则,加载 Spring 的类加载器也会用来加载业务类,但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,这该怎么办呢?

        于是 线程上下文加载器 登场了,它其实是一种 类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此 Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。

十三、SPI(service provider interface)

        解耦,灵活扩展的技术,在不改原有核心代码的基础上,新增配置和类,完成热插拔的实现,跟策略模式有点像

应用:JDBC、线程的上下文类加载器、破坏双亲委派

        Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。这个说法可能比较晦涩,但是拿我们常用的数据库驱动加载来说,就比较好理解了。在使用 JDBC 写程序之前,通常会调用下面这行代码,用于加载所需要的驱动类。

Class.forName("com.mysql.jdbc.Driver")

        这只是一种初始化模式,通过 static 代码块显式地声明了驱动对象,然后把这些信息,保存到底层的一个 List 中,这明显就是一个接口编程的思路。

        但是你会发现,即使删除了 Class.forName 这一行代码,也能加载到正确的驱动类,什么都不需要做,非常的神奇,它是怎么做到的呢?MySQL 的驱动代码,就是在这里实现的。

路径:mysql-connector-java-8.0.11.jar!/META-INF/services/java.sql.Driver

里面的内容是:com.mysql.cj.jdbc.Driver

com.mysql.cj.jdbc.Driver

        通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。

        SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。

这种方式,同样打破了双亲委派的机制。

DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。

而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。这就比较尴尬了,虽然凡事都要祖先过问,但祖先没有能力去做这件事情,

怎么办?

跟踪代码,来看一下。

使用线程上下文类加载器

通过代码你可以发现它把当前的类加载器,设置成了线程的上下文类加载器。那么,对于一个刚刚启动的应用程序来说,它当前的加载器是谁呢?也就是说,启动 main 方法的那个加载器,到底是哪一个?

所以我们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中找到以下代码。

到此为止,事情就比较明朗了,当前线程上下文的类加载器,是应用程序类加载器。使用它来加载第三方驱动。

总结一下:

第一, 可以让你更好的看到一个打破规则的案例(虽然应该是属于 BootStrap 类加载器加载的,但是还是在 app 类加载器去加载的它)。

第二, 这个问题面试时出现的几率也是比较高的,你需要好好理解。

十四、OSGI

OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。OSGi 是服务平台的规范,旨在用于需要长运行时间、动态更新和对运行环境破坏最小的系统。

OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但我们不难想象,这种与直觉相违背的加载方式,这些都是由专用的类加载器来实现的。

随着 JPMS 的发展(JDK9 引入的,旨在为 Java SE 平台设计、实现一个标准的模块系统),现在的 OSGi,深入研究意义已经不是很大了。OSGi 是一个庞大的话题(技术上),你只需要了解到,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。

OSGI 一般的公司玩不转,都是阿里这些大公司在用。从大家研究技术的角度上来,就算你去这些公司,再去学习也没问题(阿里不可能要求一个小厂出来的程序员对 OSGI 精通)。主要精力还是把放在类加载、双亲委派,以及如何打破这些问题解决即可。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值