JVM-类加载机制

一.类的生命周期

  • 类加载的过程包括了加载验证准备解析初始化五个阶段。

  • 这五个阶段中,加载,验证,准备,初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始

  • 需要注意的是这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉的混合进行的,通常在一个阶段执行的过程中调用或者激活另一个阶段

类加载的过程:

假如我的方法区中之前已经加载过了这个类,代表第二次new一个对象的时候就不需要再重新加载验证准备解析初始化了,而是直接调用

1.1 加载

加载阶段需要完成三件事:

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

类加载器:加载阶段是开发者可控性最强的阶段,类加载器可以是系统系统的,也可以是自定义。

加载方式:类的二进制字节流并没有限定说必须从Class文件获取,其他获取的渠道举例:

  • 从本地文件系统加载
  • 从数据库中获取
  • 从zip,jar等文件中获取
  • 从网络下载等

1.2 验证

验证是连接阶段的第一步,验证的主要目的是按照虚拟机的要求去检查Class字节流,确保这个字节流是符合要求的,不存在安全性问题。

验证需要完成如下工作:

  • **文件格式验证:**例如是否以魔数0xCAFEBABE开头;版本号等能否被虚拟机执行。

  • **元数据验证:**进行语义分析,确保符合]ava语言规范。例如这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法

  • **字节码验证:**通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • **符号引用验证:**符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验 例如:根据符号引用描述的名字能否找到对应的类;或者符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问等。

1.3 准备

准备阶段主要是为类的静态变量(Static变量)分配内存,并将其初始化为默认值(0,0L,null,false)

注意点:

  • 准备阶段只给类变量分配内存,不会给实例变量分配内存

    java 复制代码
    public class Student {
        // 实例变量(或者类成员变量),不被赋值
        //内存分配需要等到初始化阶段才开始
        private String name;
        private int age;
        private String studentId;
    
        // 类变量(静态变量)
        // 它不属于任何一个学生对象,而是属于Student这个类
        // 用于统计所有学生的数量
        private static int studentCount = 0;}
  • 准备阶段正常只会赋零值,准备阶段后,value=0;

java 复制代码
public static int value=123;//准备阶段value为0
  • 例外是说加了final这种,会直接赋初始值;准备阶段后value=123
java 复制代码
public static final int value=123;//value=123

1.4 解析

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

  • **符号引用:**符号引用以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,各种虚拟机实现的内存布局可以不相同
  • 直接引用:直接引用是可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。

1.5 初始化

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。

初始化是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
java 复制代码
public static int value=123;
  • 使用静态代码块为类变量指定初始值
java 复制代码
public static int value;
static{
  value=123;
}

初始化步骤:

1.如果这个类还没有被加载和连接,则程序先加载并连接该类

2.如果该类的直接父类还没有被初始化,则先初始化其直接父类

3.如果类中有初始化语句,则系统依次执行这些初始化语句

**类初始化时机:**只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下总结:

二.类加载器和类加载机制

2.1类加载器

  • 通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器(classloader)。
  • Java程序中,对于任意一个类,都必须由他的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。
  • 如果一个类被两个不同的加载器加载,即使是来源于同一个Class文件,那这也是两个不同的类;即类的加载器有很多种,也可以自定义。一个 class 文件是可以被不同加载器加载的,但加载之后会被看成不同的类。主要体现在,Class对象的equals()方法,isInstance()方法的返回结果,以及使用instance-of关键字做对象所属关系判定等各种情况。
java 复制代码
//了解即可
package jvmload;

import java.io.IOException;
import java.io.InputStream;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //1.自定义类加载器
        ClassLoader myLoader = new ClassLoader(){
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                // 对于系统类,仍然委托给父加载器
                if (name.startsWith("java.")) {
                    return super.loadClass(name);
                }
                try {
                    String fileName = name.replace('.', '/') + ".class";
                    InputStream is = getClass().getClassLoader().getResourceAsStream(fileName);
                    if(is == null) {
                        //return super.loadClass(name);
                        // 不委托给父加载器,直接抛出异常
                        throw new ClassNotFoundException(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        //2.利用自定义类加载器加载User类,并生成一个对象实例:selfLoadUser
        Object selfLoadUser = myLoader.loadClass("jvmload.User").newInstance();
        System.out.println(selfLoadUser.getClass());

        //3.判断selfLoadUser对象实例是否为User类的对象实例
        System.out.println(selfLoadUser instanceof User);
}}

结果分析:

  • 现象:selfLoadUser.getClass:输出的类名是jvmload.User,但是selfLoadUser对象却不是User类的实例(false)
  • 原因:虽然都是同一个User类源文件,但是由于加载器不同,selfLoadUser是自定义加载器加载的,而最后判定的User类是默认的系统加载器加载的所以selfLoadUser并不是该User类的实例

2.2 双亲委派模型

三层类加载器&双亲委派模型(JDK8及以前)

  1. 启动类加载器:Bootstrap ClassLoader
  • 用于加载 Java 的核心类
  • 它不是一个 Java 类,是由底层的 C++ 实现。因此,启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。Bootstrap ClassLoaderparent 属性为 null
  • 负责加载存放在<JAVA HOME>\Iib目录,或者被Xbootclasspath参数所指定的路径中存放的,而且是]ava虚拟机能够识别的类库加载到虚拟机的内存中,

2. 扩展类加载器:

  • 这个类加载器是在类sun,misc.Launcher$ExtClassLoader 中以]ava代码的形式实现的。
  • 它负责加载<JAVA HOM E>\liblext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库

3. 应用类加载器

  • sun.misc.Launcher$AppClassLoader 实现
  • 负责在 JVM 启动时加载用户类路径上的指定类库

4.用户自定义类加载器

  • 当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器
  • 自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法

**ClassLoader**里面有 3 个重要的方法,即

  1. loadClass()
  2. findClass()
  3. defineClass()

实现双亲委派的代码都集中在 java.lang.ClassLoader 的**loadClass()** 方法中。

注意:类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能

这里的"等级制度"指的是类加载器之间存在一种父子层次关系,形成一个树状结构。

这里的"非继承关系"指的是这种父子层次关系,不是通过Java语言的extends关键字(继承)来实现的

  • 误解 :你可能以为ApplicationClassLoader extends ExtensionClassLoader,而ExtensionClassLoader extends BootstrapClassLoader

  • 事实完全不是这样! 它们之间没有这种直接的类继承关系。

让我们看一下Java中ClassLoader这个核心类的代码(简化版):

java 复制代码
public abstract class ClassLoader {
    // 关键字段:每个类加载器都持有一个对其父加载器的"引用"
    private final ClassLoader parent;

    // 在构造类加载器时,可以指定其父加载器
    protected ClassLoader(ClassLoader parent) {
        this.parent = parent;
    }
}

可以看到:

  • ExtensionClassLoaderApplicationClassLoader直接继承自ClassLoader这个抽象基类

  • 它们之间的关系(谁是爸爸,谁是儿子)是通过在创建对象时,通过构造函数传入一个parent引用来建立的。

例如,在JVM内部,创建ApplicationClassLoader时,会这样写(概念上):

java 复制代码
ApplicationClassLoader appLoader = new ApplicationClassLoader(extensionLoader); // 把扩展加载器作为父加载器传入

所以,ApplicationClassLoaderExtensionClassLoader兄弟关系,都继承自ClassLoader ,但通过parent字段,前者将后者视为"父加载器"。

"组合"是面向对象设计的一个原则,意思是一个类中包含另一个类的对象(引用),通过调用这个对象的方法来复用其功能,而不是通过继承。

在双亲委派模型中,正是通过组合来实现的:

  • 每个类加载器对象内部,都组合 了一个parent字段(指向其父加载器)。

  • 当需要加载一个类时(调用loadClass方法),子加载器并不会自己立即去加载,而是会调用parent.loadClass(...),将任务委托给组合进来的父加载器去完成。

**双亲委派模型的工作过程是:**如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

总结步骤如下:

1.先检查类是否已经被加载过

2.若没有加载,则调用父加载器的loadClass()方法进行加载

3.若父加载器为空,则默认使用启动类加载器作为父加载器

4.如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载

双亲委派的优点:

避免类的重复加载
  • 通过委派的方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
保证安全性
  • 通过双亲委派的方式,可以保证安全性 。因为 BootstrapClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.String,那么这个类是不会被随意替换的,除非有人跑到你的机器上,破坏你的 JDK。

双亲委派的缺点

在双亲委派中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类(类似继承的关系)。

Java 提供了很多服务提供者接口(SPI,Service Provider Interface),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。

要解决上述问题,就需要打破双亲委派原则。

相关推荐
bobogift2 小时前
【玩转全栈】----Django基本配置和介绍
java·后端
岁月玲珑2 小时前
ComfyUI如何配置启动跳转地址127.0.0.1但是监听地址是0.0.0.0,::
java·服务器·前端
007php0073 小时前
某游戏互联网大厂Java面试深度解析:Java基础与性能优化(一)
java·数据库·面试·职场和发展·性能优化·golang·php
qianbailiulimeng3 小时前
2019阿里java面试题(一)
java·后端
Bug退退退1233 小时前
ArrayList 与 LinkedList 的区别
java·数据结构·算法
LBuffer3 小时前
破解入门学习笔记题三十四
java·笔记·学习
缺点内向3 小时前
Java: 如何在Excel中添加或删除分页符?
java·excel
m0_521329033 小时前
java-File的创建和删除
java
August_._4 小时前
【JAVA】基础(一)
java·开发语言·后端·青少年编程