JVM【类的加载过程(类的生命周期)详解】

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

概述

在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义引用数据类型则需要进行类的加载

按照 Java 虚拟机规范从 class 文件到加载到内存中的类到类卸载出内存为止它的整个生命周期包括如下 7 个阶段
在这里插入图片描述
其中验证、准备、解析 3 个部分统称为链接Linking

从程序中类的使用过程看

在这里插入图片描述

大厂面试题

蚂蚁金服
描述一下 JVM 加载 Class 文件的原理机制
一面类加载过程

百度
类加载的时机
java 类加载过程
简述 java 类加载机制

腾讯
JVM 中类加载机制类加载过程

滴滴
JVM 类加载机制

美团
Java 类加载过程
描述一下 jvm 加载 class 文件的原理机制

京东
什么是类的加载
哪些情况会触发类的加载
讲一下 JVM 加载一个类的过程 JVM 的类加载机制是什么

过程一Loading加载阶段

加载完成的操作

加载的理解
所谓加载简而言之就是将Java类的字节码文件加载到机器内存中并在内存中构建出Java类的原型——类模板对象。所谓类模板对象其实就是 Java 类在]VM 内存中的一个快照JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中这样]VM 在运行期便能通过类模板而获取 Java 类中的任意信息能够对 Java 类的成员变量进行遍历也能进行 Java 方法的调用。

反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来则 JVM 在运行期也无法反射。

加载完成的操作

加载阶段简言之查找并加载类的二进制数据生成Class的实例

在加载类时Java 虚拟机必须完成以下 3 件事情

通过类的全名获取类的二进制数据流。

解析类的二进制数据流为方法区内的数据结构Java 类模型

创建 java.lang.Class 类的实例表示该类型。作为方法区这个类的各种数据的访问入口

二进制流的获取方式

对于类的二进制数据流虚拟机可以通过多种途径产生或获得。只要所读取的字节码符合 JVM 规范即可

  • 虚拟机可能通过文件系统读入一个 class 后缀的文件**最常见**
  • 读入 jar、zip 等归档数据包提取类文件。
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于 HTTP 之类的协议通过网络进行加载
  • 在运行时生成一段 class 的二进制信息等

在获取到类的二进制信息后Java 虚拟机就会处理这些数据并最终转为一个 java.lang.Class 的实例。
如果输入数据不是 ClassFile 的结构则会抛出 ClassFormatError。

类模型与 Class 实例的位置

类模型的位置

加载的类在 JVM 中创建相应的类结构类结构会存储在方法区JDKl.8 之前永久代J0Kl.8 及之后元空间。

Class 实例的位置

类将.class 文件加载至元空间后会在堆中创建一个 Java.lang.Class 对象用来封装类位于方法区内的数据结构该 Class 对象是在加载类的过程中创建的每个类都对应有一个 Class 类型的对象。
在这里插入图片描述

Class clazz = Class.forName("java.lang.String");
//获取当前运行时类声明的所有方法
Method[] ms = clazz.getDecla#FF0000Methods();
for (Method m : ms) {
    //获取方法的修饰符
    String mod = Modifier.toString(m.getModifiers());
    System.out.print(mod + "");
    //获取方法的返回值类型
    String returnType = (m.getReturnType()).getSimpleName();
    System.out.print(returnType + "");
    //获取方法名
    System.out.print(m.getName() + "(");
    //获取方法的参数列表
    Class<?>[] ps = m.getParameterTypes();
    if (ps.length == 0) {
        System.out.print(')');
    }
    for (int i = 0; i < ps.length; i++) {
        char end = (i == ps.length - 1) ? ')' : ',';
        //获取参教的类型
        System.out.print(ps[i].getSimpleName() + end);
    }
}

数组类的加载

创建数组类的情况稍微有些特殊因为数组类本身并不是由类加载器负责创建而是由 JVM 在运行时根据需要而直接创建的但数组的元素类型仍然需要依靠类加载器去创建。创建数组类下述简称 A的过程

1.如果数组的元素类型是引用类型那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型
2.JVM 使用指定的元素类型和数组维度来创建新的数组类。
如果数组的元素类型是引用类型数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。

过程二Linking链接阶段

环节 1链接阶段之 Verification验证

当类加载到系统后就开始链接操作验证是链接操作的第一步。

它的目的是保证加载的字节码是合法、合理并符合规范的。

验证的步骤比较复杂实际要验证的项目也很繁多大体上 Java 虚拟机需要做以下检查如图所示。
在这里插入图片描述
整体说明
验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证以及符号引用验证等。

  • 其中格式验证会和加载阶段一起执行。验证通过之后类加载器才会成功将类的二进制数据信息加载到方法区中。
  • 格式验证之外的验证操作将会在方法区中进行。
    链接阶段的验证虽然拖慢了加载速度但是它避免了在字节码运行时还需要进行各种检查。磨刀不误砍柴工

具体说明

1.格式验证是否以魔数 0XCAFEBABE 开头主版本和副版本号是否在当前 Java 虚拟机的支持范围内数据中每一个项是否都拥有正确的长度等。

2.语义检查Java 虚拟机会进行字节码的语义检查但凡在语义上不符合规范的虚拟机也不会给予验证通过。比如

  • 是否所有的类都有父类的存在在 Java 里除了 object 外其他类都应该有父类
  • 是否一些被定义为 final 的方法或者类被重写或继承了
  • 非抽象类是否实现了所有抽象方法或者接口方法

3.字节码验证Java 虚拟机还会进行字节码验证字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析判断字节码是否可以被正确地执行。比如

  • 在字节码的执行过程中是否会跳转到一条不存在的指令
  • 函数的调用是否传递了正确类型的参数
  • 变量的赋值是不是给了正确的数据类型等

栈映射帧StackMapTable就是在这个阶段用于检测在特定的字节码处其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是100%准确地判断一段字节码是否可以被安全执行是无法实现的因此该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查虚拟机也不会正确装载这个类。但是如果通过了这个阶段的检查也不能说明这个类是完全没有问题的。

在前面3次检查中已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。

4.符号引用的验证校验器还将进符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此在验证阶段虚拟机就会检查这些类或者方法确实是存在的并且当前类有权限访问这些数据如果一个需要使用类无法在系统中找到则会抛出 NoClassDefFoundError如果一个方法无法被找到则会抛出 NoSuchMethodError。此阶段在解析环节才会执行。

环节 2链接阶段之 Preparation准备

准备阶段Preparation简言之为类的静态变分配内存并将其初始化为默认值。

当一个类验证通过时虚拟机就会进入准备阶段。在这个阶段虚拟机就会为这个类分配相应的内存空间并设置默认初始值。Java 虚拟机为各类型变量默认的初始值如表所示。
在这里插入图片描述
Java 并不支持 boolean 类型对于 boolean 类型内部实现是 int由于 int 的默认值是 0故对应的boolean 的默认值就是 false。

注意

1.这里不包含基本数据类型的字段用static final修饰的情况因为final在编译的时候就会分配了准备阶段会显式赋值。

// 一般情况static final修饰的基本数据类型、字符串类型字面量会在准备阶段赋值
private static final String str = "Hello world";
// 特殊情况static final修饰的引用类型不会在准备阶段赋值而是在初始化阶段赋值
private static final String str = new String("Hello world");

2.注意这里不会为实例变量分配初始化类变量会分配在方法区中而实例变量是会随着对象一起分配到 Java 堆中。
3.在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

环节 3链接阶段之 Resolution解析

在准备阶段完成后就进入了解析阶段。
解析阶段Resolution简言之将类、接口、字段和方法的符号引用转为直接引用。

具体描述

符号引用就是一些字面量的引用和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在 Class 类文件中通过常量池进行了大量的符号引用。但是在程序实际运行时只有符号引用是不够的比如当如下 println()方法被调用时系统需要明确知道该方法的位置。

举例

输出操作 System.out.println()对应的字节码

invokevirtual #24 <java/io/PrintStream.println>

在这里插入图片描述
以方法为例Java 虚拟机为每个类都准备了一张方法表将其所有的方法都列在表中当需要调用一个类的方法的时候只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作符号引用就可以转变为目标方法在类中方法表中的位置从而使得方法被成功调用

Initialization初始化阶段

简言之为类的静态变量赋予正确的初始值。

具体描述

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题那么表示类可以顺利装载到系统中。此时类才会开始执行Java字节码。(即到了初始化阶段才真正开始执行类中定义的Java程序代码。)
初始化阶段的重要工作是执行类的初始化方法clinit>方法

  • 该方法仅能由java编译器生成并由JVM调用程序开发者无法自定
    义一个同名的方法更无法直接在Java程序中调用该方法虽然该
    方法也是由字节码指令所组成
  • 它是由类静态成员的赋值语句以及static语句块合并产生的。

说明

1 .在加载一个类之前虚拟机总是会试图加载该类的父类因此父类的总是在子类之前被调用。也就是说父类的 static块优先级高于子类。
2.Java编译器并不会为所有的类都产生< clinit>初始化方法。
哪些类在编译为字节码后字节码文件中将不会包括clinit>方法

  • 一个类中并没有声明任何的类变量也没有静态代码块时
  • 一个类中声明类变量但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
  • 一个类中包含 static final修饰的基本数据类型的字段这些类字段初始化语句采用编译时常量表达式

static 与 final 的搭配问题

说明使用 static+ final 修饰的字段的显式赋值的操作到底是在哪个阶段进行的赋值

情况 1在链接阶段的准备环节赋值

情况 2在初始化阶段()中赋值

结论 在链接阶段的准备环节赋值的情况

对于基本数据类型的字段来说如果使用 static final 修饰则显式赋值(直接赋值常量而非调用方法通常是在链接阶段的准备环节进行

对于 String 来说如果使用字面量的方式赋值使用 static final 修饰的话则显式赋值通常是在链接阶段的准备环节进行

在初始化阶段()中赋值的情况 排除上述的在准备环节赋值的情况之外的情况。

最终结论使用 static+final 修饰且显示赋值中不涉及到方法或构造器调用的基本数据类到或 String 类型的显式财值是在链接阶段的准备环节进行。

public static final int INT_CONSTANT = 10;                                // 在链接阶段的准备环节赋值
public static final int NUM1 = new Random().nextInt(10);                  // 在初始化阶段clinit>()中赋值
public static int a = 1;                                                  // 在初始化阶段<clinit>()中赋值

public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);     // 在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100);           // 在初始化阶段<clinit>()中概值

public static final String s0 = "helloworld0";                            // 在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");                // 在初始化阶段<clinit>()中赋值
public static String s2 = "hellowrold2";                                  // 在初始化阶段<clinit>()中赋值

()的线程安全性

对于()方法的调用也就是类的初始化虚拟机会在内部确保其多线程环境中的安全性。

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步如果多个线程同时去初始化一个类那么只会有一个线程去执行这个类的()方法其他线程都需要阻塞等待直到活动线程执行()方法完毕。

正是因为函数()带锁线程安全的因此如果在一个类的()方法中有耗时很长的操作就可能造成多个线程阻塞引发死锁。并且这种死锁是很难发现的因为看起来它们并没有可用的锁信息。

如果之前的线程成功加载了类则等在队列中的线程就没有机会再执行()方法了。那么当需要使用这个类时虚拟机会直接返回给它已经准备好的信息。

类的初始化情况主动使用 vs 被动使用

Java 程序对类的使用分为两种主动使用和被动使用。

主动使用

Class 只有在必须要首次使用的时候才会被装载Java 虚拟机不会无条件地装载 Class 类型。Java 虚拟机规定一个类或接口在初次使用前必须要进行初始化。这里指的“使用”是指主动使用主动使用只有下列几种情况即如果出现如下的情况则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。

1.实例化当创建一个类的实例时比如使用 new 关键字或者通过反射、克隆、反序列化。

/**
 * 反序列化
 */
Class Order implements Serializable {
    static {
        System.out.println("Order类的初始化");
    }
}

public void test() {
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
        // 序列化
        oos = new ObjectOutputStream(new FileOutputStream("order.dat"));
        oos.writeObject(new Order());
        // 反序列化
        ois = new ObjectInputStream(new FileOutputStream("order.dat"));
        Order order = ois.readObject();
    }
    catch (IOException e){
        e.printStackTrace();
    }
    catch (ClassNotFoundException e){
        e.printStackTrace();
    }
    finally {
        try {
            if (oos != null) {
                oos.close();
            }
            if (ois != null) {
                ois.close();
            }
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

2.静态方法当调用类的静态方法时即当使用了字节码 invokestatic 指令。

3.静态字段当使用类、接口的静态字段时final 修饰特殊考虑比如使用 getstatic 或者 putstatic 指令。对应访问变量、赋值变量操作

public class ActiveUse {
	@Test
    public void test() {
        System.out.println(User.num);
    }
}

class User {
    static {
        System.out.println("User类的初始化");
    }
    public static final int num = 1;
}

4.反射当使用 java.lang.reflect 包中的方法反射类的方法时。比如Class.forName(“com.atguigu.java.Test”)

5.继承当初始化子类时如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。
当 Java 虚拟机初始化一个类时要求它的所有父类都已经被初始化但是这条规则并不适用于接口。

在初始化一个类时并不会先初始化它所实现的接口
在初始化一个接口时并不会先初始化它的父接口
因此一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时才会导致该接口的初始化。
6.default 方法如果一个接口定义了 default 方法那么直接实现或者间接实现该接口的类的初始化该接口要在其之前被初始化。

interface Compare {
	public static final Thread t = new Thread() {
        {
            System.out.println("Compare接口的初始化");
        }
    }
}

7.main 方法当虚拟机启动时用户需要指定一个要执行的主类包含 main()方法的那个类虚拟机会先初始化这个主类。
VM 启动的时候通过引导类加载器加载一个初始类。这个类在调用 public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载链接和初始化。
8.MethodHandle当初次调用 MethodHandle 实例时初始化该 MethodHandle 指向的方法所在的类。涉及解析 REF getStatic、REF_putStatic、REF invokeStatic 方法句柄对应的类

被动使用

除了以上的情况属于主动使用其他的情况均属于被动使用。被动使用不会引起类的初始化。

也就是说并不是在代码中出现的类就一定会被加载或者初始化。如果不符合主动使用的条件类就不会初始化。

1.静态字段当通过子类引用父类的静态变量不会导致子类初始化只有真正声明这个字段的类才会被初始化。

public class PassiveUse {
 	@Test
    public void test() {
        System.out.println(Child.num);
    }
}

class Child extends Parent {
    static {
        System.out.println("Child类的初始化");
    }
}

class Parent {
    static {
        System.out.println("Parent类的初始化");
    }

    public static int num = 1;
}

2.数组定义通过数组定义类引用不会触发此类的初始化

Parent[] parents= new Parent[10];
System.out.println(parents.getClass());
// new的话才会初始化
parents[0] = new Parent();

3.引用常量引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。

public class PassiveUse {
    public static void main(String[] args) {
        System.out.println(Serival.num);
        // 但引用其他类的话还是会初始化
        System.out.println(Serival.num2);
    }
}

interface Serival {
    public static final Thread t = new Thread() {
        {
            System.out.println("Serival初始化");
        }
    };

    public static int num = 10;
    public static final int num2 = new Random().nextInt(10);
}

4.loadClass 方法调用 ClassLoader 类的 loadClass()方法加载一个类并不是对类的主动使用不会导致类的初始化。

Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.test.java.Person");

扩展
-XX:+TraceClassLoading追踪打印类的加载信息

过程四类的 Using使用

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化 3 个类加载步骤。一旦一个类型成功经历过这 3 个步骤之后便“厉事俱备只欠东风”就等着开发者使用了。

开发人员可以在程序中访问和调用它的静态类成员信息比如静态字段、静态方法或者使用 new 关键字为其创建对象实例。

过程五类的 Unloading卸载

类、类的加载器、类的实例之间的引用关系

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

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

类的生命周期

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

一个类何时结束生命周期取决于代表它的Class对象何时结束生命周期。

具体例子

在这里插入图片描述
loader1 变量和 obj 变量间接应用代表 Sample 类的 Class 对象而 objClass 变量则直接引用它。

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

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

类的卸载

1启动类加载器加载的类型在整个运行期间是不可能被卸载的jvm 和 jls 规范

2被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到其达到 unreachable 的可能性极小。

3被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想稍微复杂点的应用场景中比如很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能被加载的类型在运行期间也是几乎不太可能被卸载的至少卸载的时间是不确定的。

综合以上三点一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来开发者在开发代码时候不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能。

回顾方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容常量池中废弃的常量不再使用的类型
HotSpot 虚拟机对常量池的回收策略是很明确的只要常量池中的常量没有被任何地方引用就可以被回收。
判定一个常量是否“废弃”还是相对简单而要判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件

  • 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景如OSGi、JSP的重加载等否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。
    Java 虚拟机被允许对满足上述三个条件的无用类进行回收这里说的仅仅是“被允许”而并不是和对象一样没有引用了就必然会回收。

再谈类的加载器

概述

类加载器是JVM执行类加载机制的前提。

ClassLoader的作用

ClassLoader是Java的核心组件所有的Class都是由ClassLoader进行加载的ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此ClassLoader在整个装载阶段只能影响到类的加载而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行则由Execution Engine决定。
在这里插入图片描述

大厂面试题

蚂蚁金服
深入分析ClassLoader双亲委派机制
类加载器的双亲委派模型是什么一面双亲委派机制及使用原因

百度
都有哪些类加载器这些类加载器都加载哪些文件
手写一个类加载器Demo
Class的forName“java.lang.String”和Class的getClassLoader的Loadclass“java.lang.String”有什么区别

腾讯
什么是双亲委派模型
类加载器有哪些

小米
双亲委派模型介绍一下

滴滴
简单说说你了解的类加载器一面讲一下双亲委派模型以及其优点

字节跳动
什么是类加载器类加载器有哪些

京东
类加载器的双亲委派模型是什么
双亲派机制可以打破吗为什么

类加载器的分类

类的加载分类显式加载 vs 隐式加载
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象而是通过虚拟机自动加载到内存中如在加载某个类的class文件时该类的class文件中引用了另外一个类的对象此时额外引用的类将通过JVM自动加载到内存中。
    在日常开发以上两种方式一般会混合使用。
//隐式加载
User user=new User();
//显式加载并初始化
Class clazz=Class.forName("com.test.java.User");
//显式加载但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent"); 

类加载器的必要性

一般情况下Java开发人员并不需要在程序中显式地使用类加载器但是了解类加载器的加载机制却显得至关重要。从以下几个方面说

  • 避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时手足无措。只有了解类加载器的 加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时就需要与类加载器打交道了。
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则以便实现一些自定义的处理逻辑。

命名空间

1.何为类的唯一性

对于任意一个类都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器都拥有一个独立的类名称空间比较两个类是否相等只有在这两个类是由同一个类加载器加载的前提下才有意义。否则即使这两个类源自同一个Class文件被同一个虚拟机加载只要加载他们的类加载器不同那这两个类就必定不相等。

2.命名空间

  • 每个类加载器都有自己的命名空间命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中不会出现类的完整名字包括类的包名相同的两个类
  • 在不同的命名空间中有可能会出现类的完整名字包括类的包名相同的两个类

在大型应用中我们往往借助这一特性来运行同一个类的不同版本。

类加载机制的基本特征

  • 双亲委派模型。但不是所有类加载都遵守这个模型有的时候启动类加载器所加载的类型是可能要加载用户代码的比如JDK内部的ServiceProvider/ServiceLoader机制用户可以在标准API框架上提供自己的实现JDK也需要提供些默认的参考实现。例如Java中JNDI、JDBC、文件系统、Cipher等很多方面都是利用的这种机制这种情况就不会用双亲委派模型去加载而是利用所谓的上下文加载器。
  • 可见性子类加载器可以访问父加载器加载的类型但是反过来是不允许的。不然因为缺少必要的隔离我们就没有办法利用类加载器去实现容器的逻辑。
  • 单一性由于父加载器的类型对于子加载器是可见的所以父加载器中加载过的类型就不会在子加载器中重复加载。但是注意类加载器“邻居”间同一类型仍然可以被加载多次因为互相并不可见。

类加载器之间的关系

Launcher类核心代码

Launcher.ExtClassLoader var1;
try {
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
    throw new InternalError("Could not create extension class loader", var10);
}

try {
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
    throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
  • ExtClassLoader的Parent类是null
  • AppClassLoader的Parent类是ExtClassLoader
  • 当前线程的ClassLoader是AppClassLoader

注意这里的Parent类并不是Java语言意义上的继承关系而是一种包含关系

类的加载器分类

JVM支持两种类型的类加载器分别为引导类加载器Bootstrap ClassLoader和自定义类加载器User-Defined ClassLoader。

从概念上来讲自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器但是Java虚拟机规范却没有这么定义而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分在程序中我们最常见的类加载器结构主要是如下情况
在这里插入图片描述

除了顶层的启动类加载器外其余的类加载器都应当有自己的“父类”加戟器。
不同类加载器看似是继承Inheritance关系实际上是包含关系。在下层加载器中包含着上层加载器的引用。

引导类加载器

启动类加载器引导类加载器Bootstrap ClassLoader

  • 这个类加载使用C/C++语言实现的嵌套在JVM内部。

  • 它用来加载Java的核心库JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容。用于提供JVM自身需要的类。

  • 并不继承自java.lang.ClassLoader没有父加载器。

  • 出于安全考虑Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

  • 加载扩展类和应用程序类加载器并指定为他们的父类加载器。
    在这里插入图片描述
    在这里插入图片描述
    使用-XX:+TraceClassLoading参数得到。
    启动类加载器使用C++编写的Yes

  • C/C++指针函数&函数指针、C++支持多继承、更加高效

  • Java由C++演变而来C++–版单继承

扩展类加载器

扩展类加载器Extension ClassLoader

  • Java语言编写由sun.misc.Launcher$ExtClassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下也会自动由扩展类加载器加载。
    在这里插入图片描述
System.out.println("***********扩展类加载器***********");
String extDirs =System.getProperty("java.ext.dirs");
for (String path :extDirs.split( regex:";")){
    System.out.println(path);
}

// 从上面的路径中随意选择一个类来看看他的类加载器是什么扩展类加载器
lassLoader classLoader1 = sun.security.ec.CurveDB.class.getClassLoader();
System.out.print1n(classLoader1); //sun.misc. Launcher$ExtCLassLoader@1540e19d

执行结果
在这里插入图片描述

系统类加载器

应用程序类加载器系统类加载器AppClassLoader

  • java语言编写由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
    在这里插入图片描述

用户自定义类加载器

用户自定义类加载器

  • 在Java的日常应用程序开发中类的加载几乎是由上述3种类加载器相互配合执行的。在必要时我们还可以自定义类加载器来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是Java开发者可以自定义类加载器来实现类库的动态加载加载源可以是本地的JAR包也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制这方面的实际应用案例举不胜举。例如著名的OSGI组件框架再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制这种机制无须重新打包发布应用程序就能实现。
  • 同时自定义加载器能够实现应用隔离例如TomcatSpring等中间件和组件框架都在内部实现了自定义的加载器并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多想不修改C/C++程序就能为其新增功能几乎是不可能的仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于ClassLoader。

测试不同的类的加载器

每个Class对象都会包含一个定义它的ClassLoader的一个引用。
获取ClassLoader的途径

// 获得当前类的ClassLoader
clazz.getClassLoader()
// 获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
// 获得系统的ClassLoader
ClassLoader.getSystemClassLoader()

说明

站在程序的角度看引导类加载器与另外两种类加载器系统类加载器和扩展类加载器并不是同一个层次意义上的加
载器引导类加载器是使用C++语言编写而成的而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载
器压根儿就不是一个Java类因此在Java程序中只能打印出空值。
数组类的Class对象不是由类加载器去创建的而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器
来说是通过Class.getClassLoader()返回的与数组当中元素类型的类加载器是一样的如果数组当中的元素类型
是基本数据类型数组类是没有类加载器的。

// 运行结果null
String[] strArr = new String[6];
System.out.println(strArr.getClass().getClassLoader());

// 运行结果sun.misc.Launcher$AppCLassLoader@18b4aac2
ClassLoaderTest[] test=new ClassLoaderTest[1];
System.out.println(test.getClass().getClassLoader());

// 运行结果null
int[]ints =new int[2];
System.out.println(ints.getClass().getClassLoader());
// 运行结果null
String[] strArr = new String[6];
System.out.println(strArr.getClass().getClassLoader());

// 运行结果sun.misc.Launcher$AppCLassLoader@18b4aac2
ClassLoaderTest[] test=new ClassLoaderTest[1];
System.out.println(test.getClass().getClassLoader());

// 运行结果null
int[]ints =new int[2];
System.out.println(ints.getClass().getClassLoader());

ClassLoader源码解析

ClassLoader与现有类的关系
除了以上虚拟机自带的加载器外用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader所有用户自定义的类加载器都应该继承ClassLoader类。

ClassLoader的主要方法

抽象类ClassLoader的主要方法

public final ClassLoader getParent()

返回该类加载器的超类加载器

public Class<?> loadClass(String name) throws ClassNotFoundException

加载名称为name的类返回结果为java.lang.Class类的实例。如果找不到类则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。

protected Class<?> findClass(String name) throws ClassNotFoundException

查找二进制名称为name的类返回结果为java.lang.Class类的实例。这是一个受保护的方法JVM鼓励我们重写此方法需要自定义加载器遵循双亲委托机制该方法会在检查完父类加载器之后被loadClass()方法调用。

  • 在JDK1.2之前在自定义类加载时总会去继承ClassLoader类并重写loadClass方法从而实现自定义的类加载类。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法而是建议把自定义的类加载逻辑写在findClass()方法中从前面的分析可知findClass()方法是在loadClass()方法中被调用的当loadClass()方法中父加载器加载失败后则会调用自己的findClass()方法来完成类加载这样就可以保证自定义的类加载器也符合双亲委托模式。

  • 需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑取而代之的是抛出ClassNotFoundException异常同时应该知道的是findClass方法通常是和defineClass方法一起使用的。一般情况下在自定义类加载器时会直接覆盖ClassLoader的findClass()方法并编写加载规则取得要加载类的字节码后转换成流然后调用defineClass()方法生成类的Class对象。

protected final Class<?> defineClass(String name, byte[] b,int off,int len)

根据给定的字节数组b转换为Class的实例off和len参数表示实际Class信息在byte数组中的位置和长度其中byte数组b是ClassLoader从外部获取的。这是受保护的方法只有在自定义ClassLoader子类中可以使用。

  • defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象ClassLoader中已实现该方法逻辑通过这个方法不仅能够通过class文件实例化class对象也可以通过其他方式实例化class对象如通过网络接收一个类的字节码然后转换为byte字节流创建对应的Class对象。
  • defineClass()方法通常与findClass()方法一起使用一般情况下在自定义类加载器时会直接覆盖ClassLoader的findClass()方法并编写加载规则取得要加载类的字节码后转换成流然后调用defineClass()方法生成类的Class对象

简单举例

protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 获取类的字节数组
    byte[] classData =getClassData(name);
    if (classData == null) {
        throw new ClassNotFoundException();
    } else{
        //使用defineClass生成class对象
        return defineClass(name,classData,θ,classData.length);
    }
}
protected final void resolveClass(Class<?> c)

链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

protected final Class<?> findLoadedClass(String name)

查找名称为name的已经被加载过的类返回结果为java.lang.Class类的实例。这个方法是final方法无法被修改。

private final ClassLoader parent;

它也是一个ClassLoader的实例这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中ClassLoader可能会将某些请求交予自己的双亲处理。

SecureClassLoader与URLClassLoader

接着SecureClassLoader扩展了ClassLoader新增了几个与使用相关的代码源对代码源的位置及其证书的验证和权限定义类验证主要指对class源码的访问权限的方法一般我们不会直接跟这个类打交道更多是与它的子类URLClassLoader有所关联。

前面说过ClassLoader是一个抽象类很多方法是空的没有实现比如findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时如果没有太过于复杂的需求可以直接继承URLClassLoader类这样就可以避免自己去编写findClass()方法及其获取字节码流的方式使自定义类加载器编写更加简洁。
在这里插入图片描述

ExtClassLoader与AppClassLoader

了解完URLClassLoader后接着看看剩余的两个类加载器即拓展类加载器ExtClassLoader和系统类加载器AppClassLoader这两个类都继承自URLClassLoader是sun.misc.Launcher的静态内部类。

sun.misc.Launcher主要被系统用于启动主应用程序ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的其类主要类结构如下
在这里插入图片描述
我们发现ExtClassLoader并没有重写loadClass()方法这足矣说明其遵循双亲委派模式而AppClassLoader重载了loadClass()方法但最终调用的还是父类loadClass()方法因此依然遵守双亲委派模式。

Class.forName()与ClassLoader.loadClass()

Class.forName()

  • Class.forName()是一个静态方法最常用的是Class.forName(String className);
  • 根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时会执行类的初始化。
Class.forName("com.atguigu.java.Helloworld");

ClassLoader.loadClass()

  • ClassLoader.loadClass()这是一个实例方法需要一个ClassLoader对象来调用该方法。
  • 该方法将Class文件加载到内存时并不会执行类的初始化直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象所以可以根据需要指定使用哪个类加载器。
Classloader cl = ......; cl.loadClass("com.atguigu.java.Helloworld");

双亲委派模型

定义与本质

类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始类的加载过程采用双亲委派机制这种机制能更好地保证Java平台的安全。

1.定义
如果一个类加载器在接到加载类的请求时它首先不会自己尝试去加载这个类而是把这个请求任务委托给父类加载器去完成依次递归如果父类加载器可以完成类加载任务就成功返回。只有父类加载器无法完成此加载任务时才自己去加载。

2.本质
规定了类加载的顺序是引导类加载器先加载若加载不到由扩展类加载器加载若还加载不到才会由系统类加载器或自定义的类加载器进行加载。
在这里插入图片描述
在这里插入图片描述

优势与劣势

1.双亲委派机制优势

  • 避免类的重复加载确保一个类的全局唯一性

Java类随着它的类加载器一起具备了一种带有优先级的层次关系通过这种层级关可以避免类的重复加载当父亲已经加载了该类时就没有必要子ClassLoader再加载一次。

  • 保护程序安全防止核心API被随意篡改

2.代码支持
双亲委派机制在java.lang.ClassLoader.loadClass(Stringboolean)接口中体现。该接口的逻辑如下
1先在当前加载器的缓存中查找有无目标类如果有直接返回。
2判断当前加载器的父加载器是否为空如果不为空则调用parent.loadClass(namefalse)接口进行加载。
3反之如果当前加载器的父类加载器为空则调用findBootstrapClassorNull(name)接口让引导类加载器进行加载。
4如果通过以上3条路径都没能成功加载则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。
双亲委派的模型就隐藏在这第2和第3步中。

3.举例
假设当前加载的是java.lang.Object这个类很显然该类属于JDK中核心得不能再核心的一个类因此一定只能由引导类加载器进行加载。当]VM准备加载javaJang.Object时JVM默认会使用系统类加载器去加载按照上面4步加载的逻辑在第1步从系统类的缓存中肯定查找不到该类于是进入第2步。由于从系统类加载器的父加载器是扩展类加载器于是扩展类加载器继续从第1步开始重复。由于扩展类加载器的缓存中也一定查找不到该类因此进入第2步。扩展类的父加载器是null因此系统调用findClassString最终通过引导类加载器进行加载。

4.思考
如果在自定义的类加载器中重写java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadclass(Stringboolean)方法抹去其中的双亲委派机制仅保留上面这4步中的第l步与第4步那么是不是就能够加载核心类库了呢

这也不行因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器还是系统类加载器抑或扩展类加载器最终都必须调用 java.lang.ClassLoader.defineclass(Stringbyte[]intintProtectionDomain)方法而该方法会执行preDefineClass()接口该接口中提供了对JDK核心类库的保护。

5.弊端

检查类是否加载的委托过程是单向的这个方式虽然从结构上说比较清晰使各个ClassLoader的职责非常明确但是同时会带来一个问题即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。

通常情况下启动类加载器中的类为系统核心类包括一些重要的系统接口而在应用类加载器中为应用类。按照这种模式应用类访问系统类自然是没有问题但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口该接口需要在应用类中得以实现该接口还绑定一个工厂方法用于创建该接口的实例而接口和工厂方法都在启动类加载器中。这时就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

6.结论

**由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型只是建议采用这种方式而已。**比如在Tomcat中类加载器所采用的加载机制就和传统的双亲委派模型有一定区别当缺省的类加载器接收到一个类的加载任务时首先会由它自行加载当它加载失败时才会将类的加载任务委派给它的超类加载器去执行这同时也是Serylet规范推荐的一种做法。

破坏双亲委派机制

双亲委派模型并不是一个具有强制性约束的模型而是Java设计者推荐给开发者们的类加载器实现方式。

在Java的世界中大部分的类加载器都遵循这个模型但也有例外的情况直到Java模块化出现为止双亲委派模型主要出现过3次较大规模“被破坏”的情况。

1.第一次破坏双亲委派机制
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前一—即JDK1.2面世以前的“远古”时代。
由于双亲委派模型在JDK 1.2之后才被引入但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在面对经存在的用户自定义类加载器的代码Java设计者们引入双亲委派模型时不得不做出一些妥协为了兼容这些已有代码无法再以技术手段避免loadClass()被子类覆盖的可能性只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass()并引导用户编写的类加载逻辑时尽可能去重写这个方法而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法双亲委派的具体逻辑就实现在这里面按照loadClass()方法的逻辑如果父类加载失败会自动调用自己的findClass()方法来完成加载这样既不影响用户按照自己的意愿去加载类又可以保证新写出来的类加载器是符合双亲委派规则的。

2.第二次破坏双亲委派机制线程上下文类加载器

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题**越基础的类由越上层的加载器进行加载**基础类型之所以被称为“基础”是因为它们总是作为被用户代码继承、调用的API存在但程序设计往往没有绝对不变的完美规则如果有基础类型又要调用回用户的代码那该怎么办呢

这并非是不可能出现的事情一个典型的例子便是JNDI服务JNDI现在已经是Java的标准服务它的代码由启动类加载器来完成加载在JDK 1.3时加入到rt.jar的肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口Service Provider InterfaceSPI的代码现在问题来了启动类加载器是绝不可能认识、加载这些代码的那该怎么办SPI在Java平台中通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI

为了解决这个困境Java的设计团队只好引入了一个不太优雅的设计线程上下文类加载器ThreadContextClassLoader。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置如果创建线程时还未设置它将会从父线程中继承一个如果在应用程序的全局范围内都没有设置过的话那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码这是一种父类加载器去请求子类加载器完成类加载的行为这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器已经违背了双亲委派模型的一般性原则但也是无可奈何的事情。 例如JNDI、JDBC、JCE、JAXB和JBI等。不过当SPI的服务提供者多于一个的时候代码就只能根据具体提供者的类型来硬编码判断为了消除这种极不优雅的实现方式在JDK6时JDK提供了java.util.ServiceLoader类以META-INF/services中的配置信息辅以责任链模式这才算是给SPI的加载提供了一种相对合理的解决方案。
在这里插入图片描述
默认上下文加载器就是应用类加载器这样以上下文加载器为中介使得启动类加载器中的代码也可以访问应用类加载器中的类。

3.第三次破坏双亲委派机制

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

IBM公司主导的JSR-291(即OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器当需要更换一个Bundle时就把Bund1e连同类加载器一起换掉以实现代码的热替换。在oSGi环境下类加载器不再双亲委派模型推荐的树状结构而是进一步发展为更加复杂的网状结构

当收到类加载请求时OSGi将按照下面的顺序进行类搜索

1将以java.∗开头的类委派给父类加载器加载。
2否则将委派列表名单内的类委派给父类加载器加载。

3否则将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4否则查找当前Bundle的ClassPath使用自己的类加载器加载。
5否则查找类是否在自己的Fragment Bundle中如果在则委派给Fragment Bundle的类加载器加载。
6否则查找Dynamic Import列表的Bundle委派给对应Bund1e的类加载器加载。
7否则类查找失败。

说明只有开头两点仍然符合双亲委派模型的原则其余的类查找都是在平级的类加载器中进行的

小结这里我们使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为但这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由突破旧有原则无疑是一种创新。

正如OSGi中的类加载器的设计不符合传统的双亲委派的类加载器架构且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议但对这方面有了解的技术人员基本还是能达成一个共识认为OSGi中对类加载器的运用是值得学习的完全弄懂了OSGi的实现就算是掌握了类加载器的精粹。

热替换的实现

热替换是指在程序的运行过程中不停止服务只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断修改必须立即表现正在运行的系统之中。基本上大部分脚本语言都是天生支持热替换的比如PHP只要替换了PHP源文件这种改动就会立即生效而无需重启Web服务器。

但对Java来说热替换并非天生就支持如果一个类已经加载到系统中通过修改类文件并无法让系统再来加载并重定义这个类。因此在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。

注意由不同ClassLoader加载的同名类属于不同的类型不能相互转换和兼容。即两个不同的ClassLoader加载同一个类在虚拟机内部会认为这2个类是完全不同的。

根据这个特点可以用来模拟热替换的实现基本思路如下图所示
在这里插入图片描述

沙箱安全机制

沙箱安全机制

保证程序安全
保护Java原生的JDK代码
Java安全模型的核心就是Java沙箱sandbox。什么是沙箱沙箱是一个限制程序运行的环境。

沙箱机制就是将Java代码限定在虚拟机JVM特定的运行范围中并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离防止对本地系统造成破坏。

沙箱主要限制系统资源访问那系统资源包括什么CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱可以定制安全策略。

JDK1.0时期

在Java中将执行程序分成本地代码和远程代码两种本地代码默认视为可信任的而远程代码则被看作是不受信的。对于授信的本地代码可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中安全依赖于沙箱Sandbox机制。如下图所示JDK1.0安全模型
在这里插入图片描述

JDK1.1时期

JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍比如当用户希望远程代码访问本地系统的文件时候就无法实现。
因此在后续的Java1.1版本中针对安全机制做了改进增加了安全策略。允许用户指定代码对本地资源的访问权限。
如下图所示JDK1.1安全模型
在这里插入图片描述

JDK1.2时期

在Java1.2版本中再次改进了安全机制增加了代码签名。不论本地代码或是远程代码都会按照用户的安全策略设定由类加载器加载到虚拟机中权限不同的运行空间来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型
在这里插入图片描述

JDK1.6时期

当前最新的安全机制实现则引入了域Domain的概念。

虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域Protected Domain对应不一样的权限Permission。存在于不同域中的类文件就具有了当前域的全部权限如下图所示最新的安全模型jdk1.6
在这里插入图片描述

自定义类的加载器

1.为什么要自定义类加载器

  • 隔离加载类
    在某些框架内进行中间件与应用的模块隔离把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器内部自定义了好几种类加载器用于隔离同一个Web应用服务器上的不同应用程序。

  • 修改类加载的方式
    类的加载模型并非强制除Bootstrap外其他的加载并非一定要引入或者根据实际情况在某个时间点进行按需进行动态加载

  • 扩展加载源
    比如从数据库、网络、甚至是电视机机顶盒进行加载

  • 防止源码泄漏
    Java代码容易被编译和篡改可以进行编译加密。那么类加载也需要自定义还原加密的字节码。

2.常见的场景

  • 实现类似进程内隔离类加载器实际上用作不同的命名空间以提供类似容器、模块化的效果。例如两个模块依赖于某个类库的不同版本如果分别被不同的容器加载就可以互不干扰。这个方面的集大成者是JavaEE和OSGI、JPMS等框架。
  • 应用需要从不同的数据源获取类定义信息例如网络数据源而不是本地文件系统。或者是需要自己操纵字节码动态修改或者生成类型。

3.注意

在一般情况下使用不同的类加载器去加载不同的功能模块会提高应用程序的安全性。但是如果涉及Java类型转换则加载器反而容易产生不美好的事情。在做Java类型转换时只有两个类型都是由同一个加载器所加载才能进行类型转换否则转换时会发生异常。

实现方式

用户通过定制自己的类加载器这样可以重新定义类的加载规则以便实现一些自定义的处理逻辑。
1.实现方式

  • Java提供了抽象类java.lang.ClassLoader所有用户自定义的类加载器都应该继承ClassLoader类。
  • 在自定义ClassLoader的子类时候我们常见的会有两种做法:
    • 方式一:重写loadClass()方法
    • 方式二:重写findclass()方法
      2.对比
  • 这两种方法本质上差不多毕竟loadClass()也会调用findClass()但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法根据参数指定类的名字返回对应的Class对象的引用。
  • loadclass()这个方法是实现双亲委派模型逻辑的地方擅自修改这个方法会导致模型被破坏容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动不破坏原有的稳定结构。同时也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码从代码的复用性来看不直接修改这个方法始终是比较好的选择。
  • 当编写好自定义类加载器后便可以在程序中调用loadClass()方法来实现类加载操作。

3.说明

  • 其父类加载器是系统类加载器
  • JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加载器并重写java.lang.ClassLoader.loadClass(String)接口的除外)连JDK的核心类库也不能例外。

Java9新特性

为了保证兼容性JDK9没有从根本上改变三层类加载器架构和双亲委派模型但为了模块化系统的顺利运行仍然发生了一些值得被注意的变动。

1.扩展机制被移除扩展类加载器由于向后兼容性的原因被保留不过被重命名为平台类加载器(platform class loader)。可以通过classLoader的新方法getPlatformClassLoader()来获取。

JDK9时基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件)其中的Java类库就已天然地满足了可扩展的需求那自然无须再保留<JAVA_HOME>\lib\ext目录此前使用这个目录或者java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了。

2.平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。

现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。
在这里插入图片描述
如果有程序直接依赖了这种继承关系或者依赖了URLClassLoader类的特定方法那代码很可能会在JDK9及更高版本的JDK中崩溃。

3.在Java9中类加载器有了名称。该名称在构造方法中指定可以通过getName()方法来获取。平台类加载器的名称是platform应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(ClassLoaderTest.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());

        //获取系统类加载器
        System.out.println(ClassLoader.getSystemClassLoader());
        //获取平台类加载器
        System.out.println(ClassLoader.getPlatformClassLoader());
        //获取类的加载器的名称
        System.out.println(ClassLoaderTest.class.getClassLoader().getName());
    }
}

4.启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器以前是C++实现但为了与之前代码兼容在获取启动类加载器的场景中仍然会返回null而不会得到BootClassLoader实例。
5.类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求在委派给父加载器加载前要先判断该类是否能够归属到某一个系统模块中如果可以找到这样的归属关系就要优先委派给负责那个模块的加载器完成加载。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“JVM【类的加载过程(类的生命周期)详解】” 的相关文章