科普文:一文搞懂jvm原理(二)类加载器

概叙

科普文:一文搞懂jvm(一)jvm概叙-CSDN博客

前面我们介绍了jvm,jvm主要包括两个子系统和两个组件: Class loader(类装载器) 子系统,Execution engine(执行引擎) 子系统;Runtime data area (运行时数据区域)组件, Native interface(本地接口)组件。

在这里,我们详细描述第一个子系统:Class loader(类装载器) 子系统

jvm作用:

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里

jvm特点:

跨平台:一次编译,到处运行;这是JVM 的主要特征之一,Java 程序在编译为字节码后可以在任何支持 JVM 的平台上运行,摆脱了硬件平台的束缚,实现了"一次编译,到处运行"的理想。

自动内存管理(内存分配和回收):JVM 提供了自动的内存管理机制,包括内存分配、垃圾回收和内存优化。开发者无需手动分配和释放内存,JVM 会自动管理对象的生命周期和内存回收,通过垃圾回收器(Garbage Collector)自动回收不再使用的对象,避免了内存泄漏和悬挂指针等问题。

即时编译:JVM 通过即时编译器将热点代码动态编译成本地机器码,提高程序的执行性能。编译器可以根据程序的运行情况进行优化,使得Java应用能随着运行事件的增长而获得更高的性能。

java代码编译和运行流程

主要是两个阶段:编译和执行。

  1. 编译:javac编译器将源代码*.java编译成class文件,即java字节码文件。
  2. 执行:jvm虚拟机,即java命令执行class文件时,通过加载器加载class文件,并通过执行引擎中的解释器翻译成汇编语言(机器指令+符号表+辅助信息)执行。

JVM的组织架构

第一步:我们要将Class文件加载到内存当中,而类加载需要用到类加载子系统Class Loader来进行加载同时对应到我们的内存当中,生成一个大的Class对象并且将必要的静态属性进行初始化等等(方法区提现)

第二步:当我们真正去执行字节码指令的时候,就需要执行引擎去发挥作用,按照我们程序的字节码指令去依次执行(涉及到虚拟机栈里去局部变量表取数据,以及操作入栈),若需要创建对象的话还需要用到堆空间

第三步:当程序继续往下走的时候,还会用到程序计数器,若用到本地的C类库,还需要用到本地方法栈

下面将按照这个详细图来讲解整个过程。

类加载器

类加载器(ClassLoader):JVM会使用类加载器将字节码文件加载到内存中,并在运行时动态地链接和加载类的定义。

类的生命周期

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段,其中验证、准备、解析三个部分统称为连接。

1.类的加载阶段

类加载器所做的工作实质是把类文件从硬盘读取到jvm内存(方法区/元数据区)中,加载完成后,即可生成一个class对象。

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区(元数据)的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区(元数据)这个类的各种数据的访问入口

那么对于一些加载.class文件的方式我们可以进行一些举例说明

  1. 从本地系统中直接加载
  2. 通过网络获取,典型场景:Web Applet
  3. 从zip压缩包中读取,成为日后jar、war格式的基础
  4. 运行时计算生成,使用最多的是:动态代理技术
  5. 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
  6. 从加密文件中获取,典型的防Class文件被反编译的保护措施

2.类的连接阶段

2.类的连接阶段--验证

这一阶段设计的目的是检测Java字节码文件是否遵守了《Java虚拟机规范》约束要求。这个阶段一般不需要程序员参与。

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

验证阶段会完成以下校验:

1.文件格式验证

验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等

2.元数据验证

对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等

3.字节码验证

对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。

4.符号引用验证

发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等

验证阶段不是必须的,虽然这个阶段非常重要,但是它对程序运行期没有影响,只影响类加载的时间,也就是说程序的启动耗时。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。

3.类的连接阶段--准备

准备阶段为静态变量(static)分配内存并设置默认值。

为类的静态变量分配内存,并设为jvm默认的初值;对于非静态的变量(对象实例化时才分配内存),则不会为它们分配内存。简单说就是分内存、赋初值。注意:设置初始值为jvm默认初值,而不是程序设定。规则如下

  • 基本类型(int、long、short、char、byte、float、double)的默认值为0,boolean默认值false
  • 引用类型的默认值为null
  • 常量的默认值为我们程序中设定的值,对于final修饰的静态变量,final static int a = 100,则准备阶段中a的初值就是100,而不是0。非静态的final常量在初始化阶段赋值,比如:static int a = 5,则在准备阶段初始值就是0而非5。

在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。

  • 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化

注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。(注意图片中的堆)jdk1.7之前方法区在堆中分配;jdk1.8之后,方法区移到堆外,用直接内存,即元数据区。

模拟元数据区的oom,可以定义final修饰static的大对象字节数组。

4.类的连接阶段--解析

这一阶段的任务就是把Class文件中、常量池中的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行"连接"的过程。解析环节介绍主要以下事情

  • 将常量池内的符号引用转换为直接引用的过程
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

解析阶段主要是将常量池中的符号引用替换为直接引用。

  • 符号引用就是在字节码文件中使用编号来访问常量池中的内容。
  • 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。


5.类的连接阶段--初始化

当执行完加载阶段、链接阶段到达初始化阶段时,就会执行类构造器方法()的过程。

初始化阶段会执行静态代码块中的代码,并为静态变量赋值。

此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

类初始化阶段是类加载过程的最后一步。而也是到了该阶段,才真正开始执行类中定义的java程序代码(字节码),之前的动作都由虚拟机主导。

注意这里一定要注意这是类的初始化,不是对象的初始化哦,对象的初始化也就是创建类实例的时候执行。

jvm对类的加载时机没有明确规范,但对类的初始化时机有:**只有当类被直接引用的时候,才会触发类的初始化。**类被直接引用的情况有以下几种:

  1. 通过以下几种方式:
    • new关键字创建对象
    • 读取或设置类的静态变量(注意:在准备阶段就已经赋值的变量,读取时不会触发初始化
    • 调用类的静态方法
  2. 通过反射方式执行1里面的三种方式;
  3. 初始化子类的时候,会触发父类的初始化;
  4. 作为程序入口直接运行时(调用main方法);
  5. 接口实现类初始化的时候,会触发直接或间接实现的所有接口的初始化。

关于类的初始化,记住两句话
1、类的初始化,会自上而下运行静态代码块或静态赋值语句,非静态与非赋值的静态语句均不执行(是在类实例化对象的时候执行)。
2、如果存在父类,则父类先进行初始化,是一个典型的递归模型。

区别于对象的初始化(实例化),类的初始化所做的一切都是基于类变量或类语句的(static修饰的),也就是说执行的都是共性的抽象信息。而我们知道,类就是对象实例的抽象。

例题巩固:

  1. 例题A
  1. 例题B
  1. 例题C
  1. 例题D

类只加载一次:我们可以使用示例代码来体会一下这个说法

java 复制代码
class DeadThread{
    static{
        if(true){
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            while(true){
            }
        }
    }
}
public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + "开始");
            DeadThread dead = new DeadThread();
            System.out.println(Thread.currentThread().getName() + "结束");
        };

        Thread t1 = new Thread(r,"线程1");
        Thread t2 = new Thread(r,"线程2");

        t1.start();
        t2.start();
    }
}
//运行结果如下:
线程2开始
线程1开始
线程2初始化当前类

//程序卡死了...

6.类的连接阶段--使用

类的使用分为直接引用间接引用

直接引用与间接引用等判别条件,是看对该类的引用是否会引起类的初始化

直接引用已经在类的初始化中的有过阐述,不再赘述。而类的间接引用,主要有下面几种情况:

  1. 当引用了一个类的静态变量,而该静态变量继承自父类的话,不引起初始化
  2. 定义一个类的数组,不会引起该类的初始化;
  3. 当引用一个类的的常量时,不会引起该类的初始化

7.类的连接阶段--卸载

当类使用完了之后,类就要进入卸载阶段了。可卸载需要具备以下条件:

  1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

static关键字

static关键字修饰的数据存储在我们的方法区中的静态常量池中,static可以修饰方法、变量和代码块
static修饰方法: 指定不需要实例化 就可以激活的一个方法。this关键字不能在static方法中使用,静态方法中不能调用非静态方法,非静态方法可以调用静态方法。
static修饰变量: 指定变量被所有对象共享 ,即所有实例都可以使用该变量。变量属于这个类。
static修饰代码块: 通常用于初始化静态变量,静态代码块属于类 。没加static的代码块认为是构造代码块

执行顺序

  • 实例化对象前,先加载类(对象载入之前,一定要是类先被载入)
  • 类(或者可以说静态变量和静态代码块)在生命周期结束前,只执行一次
  • 静态变量(属性)和静态代码块谁先声明谁先执行(同一个类中)
  • 非静态变量(属性)和非静态代码块谁先声明谁先执行(同一个类中)
  • 静态构造代码块是和类同时加载的,静态构造代码块是在实例化之后执行构造方法之前执行的,构造方法是在构造代码块执行完之后才执行的。
  • 静态方法属于类的,加载完类就可以调用静态方法(可以执行多次,注意区别于静态代码块,静态代码块只会执行一次);非静态方法是属于对象的,加载完对象就可以调用非静态方法。
  • 每创建一个对象,即每载入一个对象,非静态代码块都执行一次。执行类对象的载入之前就会调用

案例一

我们来通过一个例子来验证以下上面的观点

java 复制代码
public class InitializeDemo {
    private static int k = 1;
    private static InitializeDemo t1 = new InitializeDemo("t1");
    private static InitializeDemo t2 = new InitializeDemo("t2");
    private static int i = print("i");
    private static int n = 99;
 
    {
        print("初始化块");
        j = 100;
    }
 
    public InitializeDemo(String str) {
        System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
        ++i;
        ++n;
 
    }
 
    static {
        print("静态块");
        n = 100;
    }
 
    private int j = print("j");
 
    public static int print(String str) {
        System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
        ++n;
        return ++i;
    }
 
    public static void main(String[] args) {
        InitializeDemo test = new InitializeDemo("test");
    }
}

输出结果:

复制代码

|---|----------------------|
| | 1:初始化块 i=0 n=0 |
| | 2:j i=1 n=1 |
| | 3:t1 i=2 n=2 |
| | 4:初始化块 i=3 n=3 |
| | 5:j i=4 n=4 |
| | 6:t2 i=5 n=5 |
| | 7:i i=6 n=6 |
| | 8:静态块 i=7 n=99 |
| | 9:初始化块 i=8 n=100 |
| | 10:j i=9 n=101 |
| | 11:test i=10 n=102 |

我们来逐个分析,

一开始调用main方法,main方法内实例化InitializeDemo的对象,在对象载入之前,一定要是类先被载入

所以我们先加载InitializeDemo类,加载类的同时,会加载静态变量和静态代码块,但是是按顺序执行,且只执行一次

先加载如下静态变量

|---|-----------------------------|
| | private static int k = 1; |

加载如下静态变量的时候,发现要去加载类,由于类已经被加载了,所以会实例化这个对象,这个对象实例化前,会执行非静态代码块和为非静态属性赋值,然后再执行构造方法,按在代码中顺序执行。

|---|----------------------------------------------------------------|
| | private static InitializeDemo t1 = new InitializeDemo("t1"); |

所以先执行非静态代码块的内容:

|---|------------------|
| | { |
| | print("初始化块"); |
| | j = 100; |
| | } |

输出:1:初始化块 i=0 n=0

初始的时候i的值和n的值默认值是0,执行完这个方法后会变成i=1,n=1

接着为非静态属性赋值:

|---|-------------------------------|
| | private int j = print("j"); |

输出:2:j i=1 n=1

输出时i=1,n=1,执行完这个方法后会变成i=2,n=2

然后执行构造方法

java 复制代码
public InitializeDemo(String str) {
    System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
    ++i;
    ++n;
 
}

输出:3:t1 i=2 n=2

输出时i=2,n=2,执行完这个方法后会变成i=3,n=3

t1的实例化执行结束,接着执行t2的实例化

|---|----------------------------------------------------------------|
| | private static InitializeDemo t2 = new InitializeDemo("t2"); |

结果和上述一致,按非静态代码块和非静态属性然后构造方法方法的顺序执行
输出:
4:初始化块 i=3 n=3
5:j i=4 n=4
6:t2 i=5 n=5

两个静态属性(实例化)执行完,执行如下代码

|---|--------------------------------------|
| | private static int i = print("i"); |

输出:7:i i=6 n=6

这里执行完成后,i=7,n=7

接着执行下面的代码,此时n变成了99

|---|------------------------------|
| | private static int n = 99; |

注意:执行完这行代码后,n的值就被赋成99了,i的值还是7。

接着执行静态代码块

|---|-----------------|
| | static { |
| | print("静态块"); |
| | n = 100; |
| | } |

输出:8:静态块 i=7 n=99

输出时i=7,n=99,执行完这个方法后会变成i=8,n=100

到此类加载完毕,可以看到static变量和静态代码块都按顺序执行了,然后开始实例化test对象,参考t1,t2的实例化,按非静态代码块和非静态属性然后构造方法方法的顺序执行,这里就不会再处理static相关的代码了。
输出:
9:初始化块 i=8 n=100
10:j i=9 n=101
11:test i=10 n=102

案例二

继承中的static执行顺序,看以下例子

java 复制代码
public class Test3 extends Base {
    static {
        System.out.println("test static");
    }
 
    public Test3() {
        System.out.println("test constructor");
    }
 
    public static void main(String[] args) {
        new Test3();
    }
}
 
class Base {
    static {
        System.out.println("Base static");
    }
 
    public Base() {
        System.out.println("Base constructor");
    }
}

输出结果:

|---|--------------------|
| | Base static |
| | test static |
| | Base constructor |
| | test constructor |
| | |

执行Test3的构造方法,要先加载Test3的类加载,由于Test3继承于Base,所以他要先加载父类Base,静态代码块先执行。

则会先输出:Base static

再输出:test static

再执行子类的构造方法的时候,要先执行父类的构造方法(一般是找默认的构造方法即无参构造方法,除非在子类的构造方法里指定要调用父类的构造方案),如果是多级继承,会先执行最顶级父类的构造方法,然后依次执行各级子类的构造方法**。**

所以再输出:Base constructor

然后输出:test constructor

结果就如上。

案例三

再举一个例子

java 复制代码
public class MyTest {
    MyPerson person = new MyPerson("test");//这里可以理解为成员变量辅助,,要先把MyPerson先加载到jvm中
 
    static {
        System.out.println("test static");//1
    }
 
    public MyTest() {
        System.out.println("test constructor");//5
    }
 
    public static void main(String[] args) {//main方法在MyTest类中,使用mian方法先加载MyTest的静态方法,不调用其他,
        MyClass myClass = new MyClass();//对象创建的时候,会加载对应的成员变量
    }
}
 
class MyPerson {
    static {
        System.out.println("person static");//3
    }
 
    public MyPerson(String str) {
        System.out.println("person " + str);//4  6
    }
}
 
class MyClass extends MyTest {
    MyPerson person = new MyPerson("class");//这里可以理解为成员变量辅助,要先把MyPerson先加载到jvm中
 
    static {
        System.out.println("class static");//2
    }
 
    public MyClass() {
        //默认super()
        System.out.println("class constructor");//7
    }
}

输出:

|---|---------------------|
| | test static |
| | class static |
| | person static |
| | person test |
| | test constructor |
| | person class |
| | class constructor |
| | |

下面分析执行步骤:

  1. 先看MyTest类及其静态的变量,方法和代码块会随类的加载而开辟空间,有一个静态代码块,先执行,所以输出:test static,且此时MyTest类的其他语句不执行,此时MyTest类加载完成。
  2. 接着看main方法中调用了MyClass myClass =new MyClass(),实例化了一个MyClass类的对象,这时会先加载MyClass类,而MyClass类继承于MyTest类,在加载MyClass类前,会先加载MyTest类,但是MyTest类以及其静态的变量和静态代码块已经加载(在类的生命周期只执行一次),所以返回到子类(MyClass类)的加载,这时候会调用MyClass类的静态的变量和静态代码块。所以输出:class static
  3. MyClass类加载完后,在执行MyClass类的构造方法前,先初始化对象的成员变量(先初始化父类MyTest的成员变量),所以执行父类MyTest(类已加载过,这里就直接执行成员变量的初始化)的成员变量:MyPerson person = new MyPerson("test"),于是会加载MyPerson类和其静态的变量和静态代码块。则先输出:person static
  4. 加载完MyPerson类和其静态的变量和静态代码块后,回到MyClass类开始执行非静态代码块和属性,由于MyClass继承了MyTest,所以会先初始化MyTest,初始化MyTest类的属性:MyPerson person = new MyPerson("test");会调用MyPerson类的有参构造方法,即输出:person test
  5. MyTest类的非静态属性和非静态的代码块执行完成后,然后接着执行父类构造方法,即输出:test constructor
  6. 父类MyTest构造方法执行结束,回到子类MyClass,子类再调用构造方法前,先初始化对象的成员变量MyPerson person = new MyPerson("class");,这时候会先先加载MyPerson 和其静态的变量和静态代码块,由于上述类已经加载,而且MyPerson没有非静态属性及非静态代码块,所以直接执行其有参构造方法,即输出:person class
  7. MyClass再无其他非静态属性及非静态代码块,执行无参构造方法,即输出:class constructor
  8. MyClass实例化完成,回到MyTest类,无后续代码,至此程序执行完成。

总结

使用一个类创建对象的时候,一般都是先加载类,完成类的初始化(静态变量的赋值和静态代码块的执行,按代码中的先后顺序,整个生命周期中只会执行一次),然后实例化对象,实例化对象时不再处理静态变量和静态代码块,会处理非静态属性和非静态代码块,也是按照代码中的先后顺序执行,最后才调用构造方法。如有继承关系,则按照此规则先执行父类后执行子类。

类加载顺序的三个原则是

  • 1、父类优先于子类
  • 2、属性和代码块(看先后顺序)优先于构造方法
  • 3、静态优先于非静态

整个程序执行顺序:
父类静态变量、父类静态语句块(按代码中的先后顺序执行)--> 子类静态变量、子类静态语句块(按代码中的先后顺序执行)--> 父类非静态变量、父类非静态语句块(按代码中的先后顺序执行)--> 父类构造器 --> 子类非静态变量、子类非静态语句块(按代码中的先后顺序执行)--> 子类构造器 --> 完成

来一张图更直观些。

加载器分类

类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。

  • 启动类加载器(Bootstrap ClassLoader):用于加载Java核心类库,通常位于jre包下的类。
  • 扩展类加载器:负责加载jre/lib/ext目录下的JAR包。
  • 应用程序类加载器:负责加载应用程序classpath下的类文件。
  • 自定义类加载器(User-Defined ClassLoader):用户可以根据需要创建自己的类加载器,以加载特定位置或方式的类文件。

所以将扩展类加载器、系统类加载器也认为是自定义类加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器
  • 加载扩展类和应用程序类加载器,并作为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器(也称为系统类加载器,AppClassLoader)

  • Java语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器

示例:

java 复制代码
public class ClassLoaderTest {
    public static void main(String[] args) {

        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取系统类加载器其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

        //获取扩展类加载器其上层:获取不到引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

    }
}

这些加载器分别能加载哪些路径下的文件呢?

java 复制代码
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        System.out.println("**********启动类加载器**************");
        //获取BootstrapClassLoader能够加载的api的路径
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }
    }
}
//运行结果如下:
**********启动类加载器**************
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/resources.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/rt.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jce.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/charsets.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jfr.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/classes

可以打开路径下的jsee.jar包里的Class文件反查看加载器是什么?

java 复制代码
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        
        //file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar
        //从路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);//运行结果:null
    }
}

接下来我们接着看看扩展类的加载器有哪一些

java 复制代码
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        
        System.out.println("***********扩展类加载器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }
    }
}
//运行结果如下:
***********扩展类加载器**** *** ******
D: \developer_tools\Java\jdk1.8.0_131\jre\lib\ext
C: \Windows\Sun\Java\lib\ext

同理我们打开文件路径通过Class文件反查一下加载器是什么

java 复制代码
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        
        //file: D:\developer_tools\Java\jdk1.8.0_131\jre\lib\ext
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);
    }
}
//运行结果如下:
sun.misc.Launcher$ExtClassLoader@1540e19d

用户自定义加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。

那为什么还需要自定义类加载器?

  1. 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
  2. 修改类加载的方式
  3. 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
  4. 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)

如何自定义类加载器?

  1. 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
java 复制代码
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            //将路径下的文件以流的形式存入到内存中
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                //defineClass和findClass搭配使用
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }
    //自定义流的获取方式
    private byte[] getClassFromCustomPath(String name) {
        //从自定义路径中加载指定类:细节略
        //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
        return null;
    }
}

关于ClassLoader

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

以下这些方法都不是抽象方法,可以具体的实现

java 复制代码
可以跑一下   体验一下 classloader
public class ClassLoaderTest2 {
    public static void main(String[] args) {
        try {
            //1.
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);
            //2.
            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
            System.out.println(classLoader1);

            //3.
            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
            System.out.println(classLoader2);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
//运行结果如下:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

双亲委派

类加载器的双亲委派机制

当类加载器收到某个类加载请求时,它首先会将这个加载请求委派给父类加载器去尝试加载,会一直往上递归委派。只有当父加载器无法完成这个类的加载时,才会给对应的子类去加载,一直往下尝试去加载。

java 复制代码
我们在自己的src路径下创建自己的java.lang.String类
public class String {
    //
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
}

public class StringTest {

    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello,atguigu.com");

        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}
//运行结果如下:
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2

这时我们在创建一个新的Test类来引用它,并且看看他的加载器是什么?

程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。

单独

java 复制代码
我们将代码进行修改一下,再来运行起来看看是怎么样的输出结果
package java.lang;
public class String {
    //
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
    //错误: 在类 java.lang.String 中找不到 main 方法
    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}
//运行结果如下:
错误:在类java.lang.String中找不到main方法,请将main方法定义为:
public static void main (String[] args)
否则JavaFX 应用程序类必须扩展javafx.application.Application

由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误

双亲委派机制流程图

双亲委派机制原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

双亲委派主要解决的三个问题

双亲委派机制优势

接下来我们在创建一个示例来java.lang包下看看是否能运行起来

java 复制代码
package java.lang;

public class ShkStart {

    public static void main(String[] args) {
        System.out.println("hello!");
    }
}
//运行结果如下:
java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"


即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制

在比如我们使用加载jdbc.jar 用于实现数据库连接的时候需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api

这个时候我们就要使用双清委派机制,引导类加载器把rt.jar包加载进来针对具体的第三方实现jar包时使用系统类加载器来加载

从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载

通过上面的例子,我们可以知道,双亲机制可以

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
  1. 自定义类:自定义java.lang.String 没有被加载。
  2. 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
沙箱安全机制

当我们运行自定义String类main方法的时候出现了报错,这种其实就是沙箱安全机制,不允许你在程序中破坏核心的源代码程序

如何判断两个class对象是否相同?

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  1. 类的完整类名必须一致,包括包名
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的

打破双亲委派机制

打破双亲委派的三种方式:

自定义类加载器
线程上下文类加载器
OSGi框架类加载器

加深映像

相关推荐
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp5 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶5 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb