当前位置: 首页 > news >正文

05类加载机制篇(D4_虚拟机类加载机制)

目录

一、类加载的过程

1. 加载

1.1. 加载的过程

1.2. 加载源

1.3. 类和数组加载的区别

1.4. 加载过程的注意点

2. 验证

2.1. 验证的目的

2.2. 验证的必要性

2.3. 验证的过程

1> 文件格式验证

2> 元数据验证

3> 字节码验证

4> 符号引用验证

3. 准备

4. 解析

4.1. 类或接口的解析

4.2. 字段解析

4.3. 类方法解析

4.4. 接口方法解析

5. 初始化

初始化过程的注意点

使用静态内部类的单例实现

二、类加载的时机

1. 概述

2. 类加载器

3. 自定义类加载器

3.1. 自定义类加载器步骤

3.2. 实践

3.3. 自定义类加载器的作用

三、双亲委派模型

1. 概述

2. 为什么要使用双亲委托这种模型呢?

3. 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

4. 既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

5. 破坏双亲委派模型

为什么需要破坏双亲委派?


一、类加载的过程

1. 加载

“加载”是“类加载”(Class Loading)过程的第一步。

这个加载过程主要就是靠类加载器实现的, 包括用户自定义类加载器。

1.1. 加载的过程

在加载的过程中,JVM主要做3件事情

  • 通过一个类的全限定名来获取定义此类的二进制字节流(class文件) 在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程
  • 将这个字节流的静态存储结构转化为方法区的运行时数据结构
  • 在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口

程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接 口。

1.2. 加载源

JVM规范对于加载过程给予了较大的宽松度.一般二进制字节流都从已经编译好的本地class文件中读 取,

此外还可以从以下地方读取。

  • zip包Jar、War、Ear等
  • 其它文件生成由JSP文件中生成对应的Class类.
  • 数据库中将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现 代码在集群间分发
  • 网络从网络中获取二进制字节流.典型就是Applet.
  • 运行时计算生成动态代理技术,用ProxyGenerator.generateProxyClass为特定接口生成形式为"*$Proxy"的代 理类的二进制字节流.

1.3. 类和数组加载的区别

数组也有类型,称为“数组类型”。

如:

String[] str = new String[10];

这个数组的数组类型是 [Ljava.lang.String ,而String只是这个数组的元素类型。

数组类和非数组类的类加载是不同的,具体情况如下:

  • 非数组类:是由类加载器来完成。
  • 数组类:数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但数组类与类加载器有 很密切的关系,因为数组类的元素类型最终要靠类加载器创建。

1.4. 加载过程的注意点

  • JVM规范并未给出类在方法区中存放的数据结构类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,虚拟机规范并4没有指定。
  • JVM规范并没有指定Class对象存放的位置在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类的对象,作为本 类的外部访问接口。既然是对象就应该存放在Java堆中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求 存放这个对象。HotSpot将Class对象存放在方法区。
  • 加载阶段和链接阶段是交叉的类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:加载 -> 链接 -> 初始化但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

2. 验证

验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被 反

复使用和验证过,那么可以使用 -Xverify:none 参数关闭,以缩短类加载时间。

2.1. 验证的目的

保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。

2.2. 验证的必要性

虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换 成

任意类型、避免跳转到不存在的代码行.也就是说,Java语言的安全性是通过编译器来保证的.

但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进 制

字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么 无法

确保该二进制字节流是安全的。

通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到 的

都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证!

2.3. 验证的过程

1> 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.

本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区

后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。

印证【加载和验证】是交叉进行的:

  1. 加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区
  2. 而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区

也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特 定数据

结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作

2> 元数据验证

对字节码描述信息进行语义分析,确保符合Java语法规范

3> 字节码验证

本阶段是验证过程的最复杂的一个阶段。

本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。

字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一 个类方法体的字节码没有通过字

节码验证,那一定有问题,但若一个方法通过了验证,也不能说明 它一定安全。

4> 符号引用验证

发生在JVM将符号引用转化为直接引用的时候,

这个转化动作发生在解析阶段,对类自身以外的信 息进行匹配校验,确保解析能正常执行。

3. 准备

仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用 final

修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这里也不会为实例 变量分配初始

化。类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

准备阶段主要完成两件事情:

  • 为已在方法区中的类的静态成员变量分配内存
  • 为静态成员变量设置初始值,初始值为0、false、null等

比如:

public static int x = 1000;

注意:

  1. 实际上变量x在准备阶段过后的初始值为0,而不是1000
  2. 将x赋值为1000的putstatic指令是程序被编译后,存放于类构造器方法之中

但是如果声明为:

public static final int x = 1000;

在编译阶段会为x生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将x赋值为

1000。

4. 解析

解析是虚拟机将常量池的符号引用替换为直接引用的过程。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的

CONSTANT_Class_info 、 CONSTANT_Fieldref_info 、 CONSTANT_Methodref_info 、

CONSTANT_InterfaceMethodref_info

四种常量类型。

4.1. 类或接口的解析

判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

4.2. 字段解析

对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段, 如果

有,则查找结束;

如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它 们的父接口,还没有,则按照

继承关系从上往下递归搜

索其父类,直至查找结束(优先从接口来, 然后是继承的父类.理论上是按照上述顺序进行搜索解析,但在实

际应用中,虚拟机的编译器实现 可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该

类的接口和父类中,或同 时在自己或父类的接口中出现,编译器可能会拒绝编译).

4.3. 类方法解析

对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步 骤,而且

对类方法的匹配搜索,是先搜索父类,再搜索接口。

4.4. 接口方法解析

与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。

5. 初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为 代码

设定的默认值)。

在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是 根据程序员通过程序指定

的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始 化阶段是执行类构造器()方

法的过程。

其实初始化过程就是调用类初始化方法的过程,完成对static修饰的类变量的手动赋值还有主动调用静态代

码块。

初始化过程的注意点

  • 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收 集的顺序是由语句在源文件中出现的顺序所决定的.
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块 可以赋值,但是不能访问.
public class Test {
    static {
        i=0;
        System.out.println(i);//编译失败:"非法向前引用"

    }    
    static int i = 1;
}
  • 实例构造器需要显式调用父类构造函数,而类的不需要调用父类的类构造函数,虚拟机会确 保子类的方法执行前已经执行完毕父类的方法.因此在JVM中第一个被执行的方法的类肯定是 java.lang.Object.
  • 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会为此类 生成方法.
  • 接口也需要通过方法为接口中定义的静态成员变量显示初始化。接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成方法. 不同的是,执行接口的方法不需要先执行父接口的方法.只有当父接口中的静态成员变量被使用到 时才会执行父接口的方法.
  • 虚拟机会保证在多线程环境中一个类的方法别正确地加锁,同步.当多条线程同时去初始化一个类 时,只会有一个线程去执行该类的方法,其它线程都被阻塞等待,直到活动线程执行方法完毕.其 他线程虽会被阻塞,只要有一个方法执行完,其它线程唤醒后不会再进入方法.同一个类加载器 下,一个类型只会初始化一次.

使用静态内部类的单例实现

public class Student {
    private Student() {}
    /*
     * 此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
     */
    private static class SingletonFactory {
        private static Student student = new Student();
    }
    /* 获取实例 */
    public static Student getSingletonInstance() {
        return SingletonFactory.student;
    }
}

二、类加载的时机

1. 概述

什么时候开始加载,虚拟机规范并没有强制性的约束,对于其它大部分阶段究竟何时开始虚拟机规范也都

没有进行规范,这些都是交由虚拟机的具体实现来把握。所以不同的虚拟机它们开始的时机可能是不 同

的。但是对于初始化却严格的规定了有且只有四种情况必须先对类进行“初始化”(加载,验证,准 备自然

需要在初始化之前完成):

  1. 遇到 new 、 getstatic 、 putstatic 和 invokestatic 这四条指令时,如果对应的类没有初始 化,则要对对应的类先进行初始化。 这四个指令对应到我们java代码中的场景分别是:
  • new关键字实例化对象的时候;
  • 读取或设置一个类的静态字段(读取被final修饰,已在编译器把结果放入常量池的静态字 段除外) ;
  • 调用类的静态方法时。
  1. 使用 java.lang.reflect 包方法时对类进行反射调用的时候。
  2. 初始化一个类的时候发现其父类还没初始化,要先初始化其父类。
  3. 当虚拟机开始启动时,用户需要指定一个主类,虚拟机会先执行这个主类的初始化。

2. 类加载器

  • 启动类加载器(Bootstrap ClassLoader):
  • 负责加载 JAVA_HOME\lib 目录中的,
  • 或通过-Xbootclasspath参数指定路径中的,
  • 且被虚拟机认可(按文件名识别,如rt.jar)的类。
  • 由C++实现,不是ClassLoader子类
  • 扩展类加载器(Extension ClassLoader):
  • 负责加载 JAVA_HOME\lib\ext 目录中的,
  • 或通过java.ext.dirs系统变量指定路径中的类库。
  • 应用程序类加载器(Application ClassLoader):
  • 负责加载用户路径(classpath)上的类库。

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap

ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有 ClassLoader加

载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

3. 自定义类加载器

3.1. 自定义类加载器步骤

(1)继承ClassLoader

(2)重写findClass()方法

(3)调用defineClass()方法

3.2. 实践

下面写一个自定义类加载器:指定类加载路径在D盘下的lib文件夹下。

(1)在本地磁盘新建一个 Test.java 类,代码如下:

package jvm.classloader;
public class Test {
    public void say(){
        System.out.println("Hello MyClassLoader");
    }
}

(2)使用 javac -d . Test.java 命令,将生成的 Test.class 文件放到 D:/lib/jvm/classloader文件夹下。

(3)在Eclipse中自定义类加载器,代码如下:

package jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader{
    private String classpath;
    public MyClassLoader(String classpath) {
        this.classpath = classpath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte [] classDate=getData(name);          
            if(classDate==null){}          
            else{
                //defineClass方法将字节码转化为类
                return defineClass(name,classDate,0,classDate.length);
            }           
        } catch (IOException e) {          
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    //返回类的字节码
    private byte[] getData(String className) throws IOException{
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path=classpath + File.separatorChar +
        className.replace('.',File.separatorChar)+".class";
        try {
            in=new FileInputStream(path);
            out=new ByteArrayOutputStream();
            byte[] buffer=new byte[2048];
            int len=0;
            while((len=in.read(buffer))!=-1){
                out.write(buffer,0,len);
            }
            return out.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally{
            in.close();
            out.close();
        }
        return null;
    }
}

测试代码如下(在Eclipse中):

package jvm.classloader;
import java.lang.reflect.Method;

public class TestMyClassLoader {
    public static void main(String []args) throws Exception{
        //自定义类加载器的加载路径
        MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
        //包名+类名
        Class c=myClassLoader.loadClass("jvm.classloader.Test");
        if(c!=null){
            Object obj=c.newInstance();
            Method method=c.getMethod("say", null);
            method.invoke(obj, null);
            System.out.println(c.getClassLoader().toString());
        }
    }
}

输出结果如下:

3.3. 自定义类加载器的作用

JVM自带的三个加载器只能加载指定路径下的类字节码。

如果某个情况下,我们需要加载应用程序之外的类文件呢?

比如本地D盘下的,或者去加载网络上的某个 类文件,这种情况就可以使用自定义加载器了

三、双亲委派模型

1. 概述

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加

载器。

  • 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶 层的启动类加载器,
  • 只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的一个好处是:

  • 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委 托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

2. 为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态

替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免 这种情

况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用 户自定义的

ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索 类的默认算法。

3. 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例

加载的。

只有两者同时满足的情况下,JVM才认为这两个class是相同的。

就算两个class是同一份class字节 码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两

个不同class。

4. 既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的 类或jar

时。比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的

业务逻辑。

在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的 ClassLoader

5. 破坏双亲委派模型

为什么需要破坏双亲委派?

因为在某些情况下父类加载器需要加载的class文件由于受到加载范围的限制,父类加载器无法加载到 需要

的文件,这个时候就需要委托子类加载器进行加载。

而按照双亲委派模式的话,是子类委托父类加载器去加载class文件。

这个时候需要破坏双亲委派模式 才能加载成功父类加载器需要的类。也就是说父类会委托子类去加载它需

要的class文件。

以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,

比 如mysql的就写了 MySQL Connector ,这些实现类都是以jar包的形式放到classpath目录下。

那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类 (classpath

下),然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的 lib下文件,而

其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托 子类来加载Driver

实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

JDBC代码:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.Test;
public class TestJdbc {
    @Test
    public void testJdbc() {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet rs = null;
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 通过驱动管理类获取数据库链接connection = DriverManager
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ssm?characterEncoding=utf- 8", "root","root");
            // 定义sql语句 ?表示占位符
            String sql = "select * from user where id = ?";
            // 获取预处理 statement
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为设置的

            preparedStatement.setInt(1, 1);
            // 向数据库发出 sql 执行查询,查询出结果集

            rs = preparedStatement.executeQuery();
            // 遍历查询结果集

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源

            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                }
            }
        }
    }
}

待添加(JVM热加载技术)

相关文章:

  • 【数据结构】堆(挑战从零基础到进阶)
  • 团队学习—系统思考
  • plt和cv2有不同的图像表示方式和颜色通道顺序
  • c语言笔记 指针进阶
  • 机器学习12-视觉识别任务
  • 【音视频】ffplay简单过滤器
  • STM32 SPI的应用开发
  • NET431-C协议网关:跨网段·零编程PLC工业通信终极方案
  • 八卡5090服务器首发亮相!
  • idea创建测试方法测试接口是否能够正常使用
  • [Python入门学习记录(小甲鱼)]第5章 列表 元组 字符串
  • 【从模仿到超越:AIGC的崛起与AGI的终极梦想】
  • 解决:Word 保存文档失败,重启电脑后,Word 在试图打开文件时遇到错误
  • Java多线程与高并发专题——为什么 Map 桶中超过 8 个才转为红黑树?
  • 并发与IO多路复用(select)(20250306)
  • 解决Leetcode第3470题全排列IV
  • Ubuntu20.04本地配置IsaacLab 4.2.0的G1训练环境(二):训练与推理
  • ubuntu 工具操作记录(vim)
  • springboot429-基于springboot的教务管理系统(源码+数据库+纯前后端分离+部署讲解等)
  • PCB设计相关笔记
  • 衡天主机怎么做网站/seo关键词挖掘
  • 长春高铁站/app开发自学
  • 猪八戒网怎么做网站/关键词搜索量怎么查
  • 安顺建设局网站官网/网络营销渠道策略有哪些
  • 项目建设背景是什么/seo关键词优化的技巧