JVM-类的加载机制

本文详细介绍了Java虚拟机(JVM)的类加载机制,包括加载、验证、准备、解析和初始化五个阶段。类加载时机由JVM严格规定,如创建对象、静态变量引用等。加载阶段涉及加载类的二进制数据,验证阶段确保数据正确性,准备阶段为类变量分配内存并赋予默认值,解析阶段将符号引用转为直接引用,初始化阶段执行静态初始化。类加载器的工作遵循双亲委派机制,保证类的唯一性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始
化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类加载时机:

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载
(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化
(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个
部分统称为连接(Linking).
在这里插入图片描述

在加载阶段:JVM规范中没有进行约束, 这点可以交给虚拟机的具体实现来自由把握.
在初始化阶段:
JVM严格规范了有且只有以下5种情况必须立即进行初始化(初始化前,必须经过加载,验证解析,准备阶段)
1.使用new实例化对象时,读取(getstatic)和设置(putstatic)类的静态变量,静态非字面值常量时,调用静态方法(invokestatic)时
2.对内进行反射调用时, 如果类没有进行过初始化,则需要先触发其初始化
3.当初始化一个类时,如果父类没有进行初始化,首先要初始化父类.
4.启动程序所使用的main方法所在类
5.当使用1.7的动态语言支持时
以上5种称为主动引用,除此之外的引用被称为被动引用

被动引用常见情况:

  • 调用父类的静态字段,只会初始化父类,不会触发子类的初始化
  • 定义对象数组和集合,不会触发该类的初始化
  • 类A引用类B的static final 常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)
  • 通过类名获取对象,不会触发类的初始化(System.out.println(Person.class));
  • 通过Class.forName加载指定类时,如果指定参数initialize为false,也不会初始化该类
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化.

类加载过程:

加载流程: 加载->连接(验证->准备->解析)->初始化->使用->卸载

加载

在加载阶段 , JVM需要完成3件事情

  1. 查找并加载类的二进制数据(Class文件)
  2. 静态存储结构转化为方法区运行时数据结构:类的类信息
  3. Java堆生产:Class文件对应的类实例(相当于一个句柄),去访问方法区

加载.class文件的方式:

  • 从本地系统中直接加载
  • 通过网络下载class文件
  • 将java源文件动态编译为.class文件

验证

确保加载的类信息是正确的

  • 文件格式验证
    • 验证Class文件的标示:魔数(默认为:CAFE BABE)
    • 验证Class文件的版本号
    • 验证常量池(常量类型,常量类型数据结构是否正确,UTF8是否符合标准)
    • Class文件的每个部分(字段表,方法表)是否正确
  • 元数据验证(父类验证,继承验证,接口验证,final验证)
    • 这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等
  • 字节码验证(指令验证)
  • 符号引用验证(通过符号引用是否能找到类,方法,字段)
    • 通过符合引用能找到对应的类和方法
      ,符号引用中类、属性、方法的访问性是否能被当前类访问等等。

准备

类变量:一般称为静态变量
实例变量:当对象被实例化时,实例变量就被跟着被确定

为类的静态变量(static)分配内存空间并赋予初始值(默认值) ,这些变量所使用的内存都将在方法区中进行分配.

例:static int value = 2;初始化值为0.

这时候尚未开始执行任何Java方法,而把value赋值为2的putstatic指令是程序被编译后,存放于类构造器< clinit>()方法之中,所以把value赋值为2的动作将在初始化阶段才会执行。

而 static final int value = 2;

对应到常量池ConstantValue,在准备阶段必须赋值为2.

数据类型对应的零值
基本数据的零值

解析

将符号引用转化为直接引用
直接引用: 指向目标的指针或者偏移量
主要涉及:类,接口,字段,方法等.
匹配:简单名字+描述符 同时满足

类或接口的解析

public class A{
	C data; //C有可能是个类,或接口
}
  1. 如果C不是一个数组类型 , 那么会将代表N的权限定名传递给A去加载这个类C . 若在加载C的过程中出现了异常,则解析失败.
  2. 如果C是一个数组类型 ,并且数组元素类型为对象 (Integer[] data), 则按照 1中的方式先加载数组元素类型 ,接着由虚拟机生成一个代表此数组维度和元素的数组对象.
  3. 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认A是否具备对C的访问权限。

如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

字段的解析:

class A extends B implements C,D{
     private int a;  
}
  1. 先在本类去找有没有简单名称字段描述符匹配的字段,有则返回字段的引用 , 查找结束.
  2. 如果类实现了接口,递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束.
  3. 如果不是继承Object类, 再去搜索父类 , 有则返回.
  4. 否则 , 查找失败

查找可能会有异常

如果找到,没有权限:java.lang.IllegalAccessError 如果失败:java.lang.NoSuchFieldError

类方法的解析:

class A extends B implements C,D{
     public void inc();
}

类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

  1. 在本类里面去找有没有匹配的方法
  2. 父类去找
  3. 接口列表去找方法(如果找到则代表本类是抽象类)

如果找到,没有权限:java.lang.IllegalAccessError 如果失败:java.lang.NoSuchMethodError

接口方法的解析:
与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

  1. 在本类找有没有匹配的方法
  2. 父类接口去递归查找

如果失败:java.lang.NoSuchMethodError
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。

初始化

java虚拟机对类进行初始化,对静态变量赋予正确值, 真正开始执行类中定义的Java程序代码.
< init>:类的实例构造器
< clinit>:由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并.(如果没有静态变量,静态块,则没有clinit)

在类中:

< clinit>()方法与类的构造函数(或者说实例构造器< init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<
clinit>()方法执行之前,父类的< clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<
clinit>()方法的类肯定是java.lang.Object。 由于父类的<
clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,

在接口中:

接口中不能有静态块 ,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成< clinit>()方法。
但接口与类不同的是,执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。

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

Class A{
    static int i = 2;
    static{
          sout;
     }
    int n;
}

类的加载方式(整个过程)

隐式加载

  • 创建类对象
  • 使用类的静态域
  • 创建子类对象
  • 使用子类的静态域

在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件

显式加载

ClassLoader.loadClass(className),只会加载和链接,不会进行初始化
Class.forName(String name,boolean initialize,ClassLoader loader),使用loader进行加载和链接,根据initialize参数决定是否初始化

类加载器:

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

//sun.misc.Launcher
//..........
/..........
var1 = Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
//..........

问题: ExtClassLoader和AppClassLoader有什么关系?

ExtClassLoader是AppClassLoader的Parent

//代表同时只有一个load来加载class,保证class的唯一性
findBootStrapClass(String name):  //方法
synchronized(getClassLoadingLock(name)) {
    Class<?> c = findLoadedClass(class);
    if(c == null) {
          ......
          if(parent != null) {
                 //递归调用
           } else {
                  findBootStrapClass(name);  //native方法 JNI调用
            }
     }
}
  • BootStrapClassLoader(C语言编写): JAVA_HOME
    /lib/rt.jar
  • ExtClassLoader: JAVA_HOME/lib/ext/javax.* 或"java.ext.dirs"指向的目录
  • AppClassLoader:自定义的类,负责将系统类路径(CLASSPATH)中指定的类库加载到内存中
  • 用户自定义类加载器: 流,网络,数据库

双亲委派机制(安全)

在这里插入图片描述
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上.因此,所有的类加载请求最终都会被传递到顶层的启动类加载器中.
只有当父加载器在他的搜索范围中没有找到所需的类时,既无法完成加载,自加载器才会尝试自己去加载该类.
父类能加载的,不给子类加载

这种机制有如下好处:

  • 可以保证java核心类库的安全,即保证由引导类加载器加载的类不能被用户随便替换,不能自己定义一个java.lang.String 的类来替换java核心类库的java.lang.String类,否则会抛出ClassCastException。
  • 避免类的重复加载.

加载顺序: 自顶向下 AppClassLoader -> Extension ClassLoader -> BootStrap ClassLoader
检查顺序: 自底向上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值