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

news2024/11/16 17:51:29

概叙

科普文:一文搞懂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虚拟机规范》约束要求。这个阶段一般不需要程序员参与。
image.png

  • 目的在于确保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等
     

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

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

image.png
image.png

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

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

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

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

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

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

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

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

关于类的初始化,记住两句话
1、类的初始化,会自上而下运行静态代码块或静态赋值语句,非静态与非赋值的静态语句均不执行(是在类实例化对象的时候执行)。
2、如果存在父类,则父类先进行初始化,是一个典型的递归模型。
区别于对象的初始化(实例化),类的初始化所做的一切都是基于类变量或类语句的(static修饰的),也就是说执行的都是共性的抽象信息。而我们知道,类就是对象实例的抽象。

例题巩固:

  1. 例题A

image.png

  1. 例题B

image.png

  1. 例题C

image.png

  1. 例题D

image.png

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

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的代码块认为是构造代码块

执行顺序

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

案例一

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

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

然后执行构造方法

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执行顺序,看以下例子

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
结果就如上。

案例三

再举一个例子

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()方法可以获取到该类加载器

示例:

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

    }
}

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

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文件反查看加载器是什么?

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

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

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文件反查一下加载器是什么

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()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
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(不包括启动类加载器)

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

可以跑一下   体验一下 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

双亲委派

类加载器的双亲委派机制

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

我们在自己的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 类。

单独

我们将代码进行修改一下,再来运行起来看看是怎么样的输出结果
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() 方法,所以就报了上面的错误

双亲委派机制流程图
image.png

双亲委派机制原理

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

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

双亲委派机制优势


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

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实例对象不同,那么这两个类对象也是不相等的

打破双亲委派机制

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

  1.         自定义类加载器

image.png

image.png
 

  1.         线程上下文类加载器
    在这里插入图片描述
  2. OSGi框架类加载器
    在这里插入图片描述

加深映像

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1883215.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

类和对象【上】【C++】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 博主主页&#xff1a;LiUEEEEE                        …

试用笔记之-收钱吧安卓版演示源代码,收钱吧手机版感受

首先下载&#xff1a; https://download.csdn.net/download/tjsoft/89499105 安卓手机安装 如果有收钱吧帐号输入收钱吧帐号和密码。 如果没有收钱吧帐号点我的注册 登录收钱吧帐号后就可以把手机当成收钱吧POS机用了&#xff0c;还可以扫客服的付款码哦 源代码技术交流QQ:42…

Nuxt3 的生命周期和钩子函数(七)

title: Nuxt3 的生命周期和钩子函数&#xff08;七&#xff09; date: 2024/6/30 updated: 2024/6/30 author: cmdragon excerpt: 摘要&#xff1a;文章阐述了Nuxt3中Nitro生命周期钩子的使用&#xff0c;如nitro:config自定义配置、nitro:init注册构建钩子、nitro:build:be…

Python自动化,实现自动登录并爬取商品数据,实现数据可视化

关于如何使用Python自动化登录天 猫并爬取商品数据的指南&#xff0c;我们需要明确这是一个涉及多个步骤的复杂过程&#xff0c;且需要考虑到天猫的反爬虫策略。以下是一个简化的步骤指南&#xff1a; 步骤一&#xff1a;准备工作 环境准备&#xff1a;确保你的Python环境已经…

数据沿袭是止痛药还是维生素?

首先&#xff0c;这在很大程度上取决于用户组织当前的使用案例及其成熟度。 在我看来&#xff0c;数据工程师喜欢查看数据流并对依赖关系有直观的了解&#xff0c;但他们最终真的会使用数据沿袭吗&#xff1f;使用频率是多少&#xff1f;具体用例是什么&#xff1f; 从我们的观…

<电力行业> - 《第12课:配电(2)》

5 配网的指标 配电网与广大用户紧密联系&#xff0c;所以配电网是否合格还是十分重要的。 评判配电网的标准&#xff0c;主要有四个指标&#xff1a; 供电可靠性&#xff1a;供电可靠性是指针对用户连续供电的可靠程度。网损率&#xff1a;网损率可定义为电力网的电能损耗量与…

问题-小技巧-专业版Win11怎么启动电脑的休眠模式?

专业版Win11怎么启动电脑的休眠模式&#xff1f; powercfg -a powercfg -hibernate on 启用管理员面板依次输入上述命令就可以了。

短视频电商源码怎么选择

随着移动互联网的迅猛发展&#xff0c;短视频电商成为了一种热门的商业模式。很多商家和创业者都希望能够快速搭建一个短视频电商平台来推广和销售自己的产品。然而&#xff0c;选择合适的短视频电商源码并不是一件容易的事情。在选择之前&#xff0c;有一些关键因素需要考虑。…

控制器方法执行流程和 @InitBinder【Spring源码学习】

控制器方法执行流程 InitBinder 加在ControllerAdvice中 首先说明ControllerAdvice和aop没有任何关系&#xff01; 加在ControllerAdvice中只对所有控制器都生效 全局的在开始时就会保存到handlerMappingAdapter中的cache中&#xff1b; 加在Controller中 加在controller中只对…

小程序使用echarts和echarts配置项总结(全网最简单详细)

文章目录 概要小程序中使用echarts1. ec-canvas2. 下载项目3. 去echarts官网定制&#xff1a;4.点击下载5.引入使用 echarts的option配置知识点归纳整理&#xff08;还在更新&#xff09;&#xff1a;小结 概要 小程序中使用echarts&#xff08;简单详细&#xff09; 小程序中…

redis,memcached,nginx网络组件

课程目标&#xff1a; 1.网络模块要处理哪些事情 2.reactor是怎么处理这些事情的 3.reactor怎么封装 4.网络模块与业务逻辑的关系 5.怎么优化reactor? io函数 函数调用 都有两个作用&#xff1a;io检测 是否就绪 io操作 1. int clientfd accept(listenfd, &addr, &l…

[论文精读]Variational Graph Auto-Encoders

论文网址&#xff1a;[1611.07308] Variational Graph Auto-Encoders (arxiv.org) 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎…

已解决 SyntaxError: invalid syntax,Python报错原因和解决方案。

「作者简介」&#xff1a;冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础著作 《网络安全自学教程》&#xff0c;适合基础薄弱的同学系统化的学习网络安全&#xff0c;用最短的时间掌握最核心的技术。 这篇文章带大家…

【WSL2+Ubuntu+Docker Desktop】迁移到D盘

如果不会安装Ubuntu&#xff0c;可以看这篇文章 安装到C盘后先创建Ubuntu实例 下载完之后先创建实例&#xff0c;输入自己的实例名以及密码 迁移wsl-2 wsl默认保存在C:\Users<主机名>\AppData\Local下 随着在子系统上安装的软件/服务越来越多&#xff0c;C盘的空间也所…

前端——在本地搭建Vue单页应用

目录 1、安装最新node.js 2、打开命令行窗口 3、进入要保存项目的目录下 4、安装 Vue CLI 5、创建新项目&#xff0c;选择功能 5.1 新建项目 5.2 Please pick a preset 5.3 Check the features needed for your project 5.4 Choose a version of Vue.js 5.5 Use hist…

php 通过vendor文件 生成还原最新的composer.json

起因&#xff1a;因为历史原因&#xff0c;在本项目中composer.json基本算废了&#xff0c;没法直接使用composer管理扩展&#xff0c;今天尝试修复一下composer.json。 历史文件&#xff0c;可以看出来已经很久没有维护了&#xff0c;我们主要是恢复require的信息 {"na…

Linux4(Docker)

目录 一、Docker介绍 二、Docker结构 三、Docker安装 四、Docker 镜像 五、Docker 容器 六、Docker 安装nginx 七、Docker 中的MySQL部署 一、Docker介绍 Docker&#xff1a;是给予Go语言实现的开源项目。 Docker的主要目标是“Build,Ship and Run Any App,Anywhere” 也…

ROS2用c++开发参数节点通信

1.创建节点 cd chapt4/chapt4_ws/ ros2 pkg create example_parameters_rclcpp --build-type ament_cmake --dependencies rclcpp --destination-directory src --node-name parameters_basic --maintainer-name "joe" --maintainer-email "1027038527qq.com&…

Spring Security 认证流程

Spring Scurity是spring生态下用于认证和授权的框架&#xff0c;具有高度的灵活性和可扩展行&#xff0c;本节主要对Spring Security的认证过程中进行概括性的介绍&#xff0c;主要介绍在该过程中&#xff0c;会涉及到哪些组件以及每个组件所承担的职责&#xff0c;希望大家可以…

数据驱动测试DDT之Selenium读取Excel文件

&#xff08;1&#xff09;安装xlrd pip3 install xlrd &#xff08;2&#xff09;示例脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ​import pytest import xlrd def get_data(): filename"F:\\学习\\自动化测试\\selenium自动化…