jvm 类的加载过程详解

1. 加载阶段

① 加载完成的操作

所谓加载, 简而言之就是将java类的字节码文件加载到集器内存中, 并在内存中构建出java类的原型-----类模板对象, 里面存储常量池, 类字段, 类方法等信息

反射的机制即基于这一基础, 如果jvm没有将java类的生命信息存储起来, 则jvm在运行期也无法反射

加载阶段, 简言之, 查找并加载类的二进制文件, 生成class实例, 这个实例指向方法区的类模型, 执行其他操作

在加载类时, java虚拟机必须完成一下三件事 

  • 通过类的全名, 获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(java类模型)
  • 创建java.lang.class类的实例, 表示该类型, 作为方法区这个类的各种数据的访问入口

② 类模型与class实例的位置

类模型在方法区中

class实例会在堆中创建一个java.lang.class对象, 用来封装类位于方法区内的数据结构, 该Class对象是在加载类的过程中创建的, 每个类都有对应的一个Class类型的对象

2. 链接阶段

① 验证阶段

当类加载到系统后, 就开始链接操作, 验证是链接操作得第一步

他的目的就是保证加载得字节码是合法, 合理并符合规范的

  • 魔术检查 : 判断字节码文件是否是 cafebabe开头
  • 版本检查 : 主版本和父版本是否是向下兼容
  • 是否继承final
  • 是否有父类
  • 抽象方法是否有实现

② 准备阶段

主要作用 : 为类得静态变量分配内存, 并将其初始化为默认值

这里不包含基本数据类型的字段用static final修饰的情况, 因为final在编译的时候就会分配了,准备阶段会显示赋值

③ 解析阶段

解析阶段, 简言之, 将类,接口,字段和方法的符号引用转化为直接引用

以方法为例, java虚拟机为每个类都准备了一张方法表, 将其所有的方法都列在表中, 当需要调用一个类的方法的时候, 只要知道这个方法在方法表中的偏移量就可以直接调用该方法, 通过解析操作, 符号引用就可以转变为目标方法在类中方法表中的位置, 从而使得方法被成功调用

3. 初始化阶段

初始化阶段, 简言之, 为类的静态变量赋予正确的初始化值

到了初始化阶段, 才真正开始执行类中定义的java程序代码

初始化阶段的重要工作是执行类的初始化方法 : <clinit()方法>

① static 与 final的搭配问题

编译器哪些场景下不会生成<clinit()> 方法 ? 

  • 对应非静态的字段, 不管是否进行了显式赋值, 都不会生成<clinit()>方法  public int num = 1;
  • 静态的字段, 没有显示赋值, 不会生成<clinit()>方法,   public static int num;
  • 对于声明为static final的基本数据类型的字段, 不管是否进行了显示赋值, 都不会生成<clinit()>方法  public static final int num = 1;

使用static final修饰的字段的显式赋值操作, 到底是在哪个阶段进行赋值?

情况1 : 在连接阶段的准备环节赋值

情况2 : 在初始化阶段<clinit>()中赋值

结论 : 

在链接阶段的准备环节赋值的情况 : 

  1. 对于基本数据类型字段来说, 如果使用static final修饰, 显示赋值(直接赋值常量, 而非调用方法) 
  2. 对于string来说, 使用字面量的方式赋值, 使用static final修饰

在初始化阶段<clinit>()中赋值的情况 :

  • 排除上述的在准备环节赋值的情况之外的情况

最终结论 : 

在使用static final修饰, 且显示赋值中不涉及到方法或构造器调用的基本数据类型或string类型的显式赋值,是在链接阶段的初始化环节赋值

举例说明 : 

情况1 : 准备阶段赋值      情况2 : 初始化阶段赋值

public static int a = 1; 情况2

public static final int COUNT = 10;  情况1

public static final Integer COUNT = Integer.valueOf(100); 情况2

public static Integer COUNT = Integer.valueOf(100); 情况2

public static final String s = "hello";  情况1

public static final String s = new String("hello"); 情况2

public static final int num = new Random().nextInt(10); 情况2

② 主动使用 (意味着调用类的<clinit>()方法)

Class只有在必须要首次使用得时候, 才会被装载, java虚拟机不会无条件得装载class类型, jvm规定, 一个类或者接口在初次使用前, 必须要进行初始化, 这里指的"使用", 是指主动使用, 主动使用只有一下几种情况, (如果出现如下情况, 必须要进行初始化操作, 而初始化操作之前的加载, 验证 ,准备已经完成)

  • 当创建一个类的实例时, 比如使用new关键字, 或者通过反射,克隆, 反序列化
  • 当调用类的静态方法时候, 即当使用了字节码invokestatic指令
  • 当使用类,接口的静态字段是 (final修饰特殊考虑, 同之前)
  • 当使用java.lang.reflect包中的方法反射类的方法时, 比如 : Class.forName("com.hctrl.java.Test");
  • 当初始化子类时, 如果发现其父类还没有进行初始化, 则需要先触发其父类的初始化(接口不会触发)
  • 如果一个接口定义了default方法, 那么直接实现或者间接实现该接口的类的初始化, 该接口要在其之前被初始化
  • 当虚拟机启动时, 用户需要指定一个要执行的主类(包含main方法的哪个类),虚拟机会初始化这个主类
  • 当初次调用MethodHandle实例时, 初始化该MethodHandle指向的方法所在的类

③ 被动使用(并不会引起类的初始化, 意味着不调用类的<clinit>()方法)

  • 当访问一个静态字段时, 只有真正声明这个字段的类才会被初始化, 当通过子类引用父类的静态变量时, 不会导致子类的初始化
  • 通过数组定义类引用, 不会触发此类的初始化
  • 引用常量不会触发此类或接口的初始化, 因为常量在链接阶段就已经被显式赋值了
  • 调用ClassLoader类的loadClass()方法加载一个类, 并不是对类的主动使用, 不会导致类的初始化

 

4. 类的使用

 

5. 类的卸载

想要进行类的卸载, 需要使方法区中类的二进制数据结构不被使用, 因此需要断开线1, 想要断开线1, 需要先断开线2,3,5 想要断开2,3 需要先断开4,6, 因此想要卸载已加载的类需要完成一下条件

  • 该类所有的实例都已经被回收, 也就是java堆中不存在该类及其任何派生子类的实例 (线6)
  • 加载该类的类加载器已经被回收, 基本不可能 (线4)
  • 该类对应的java.lang.Class对象没有任何地方被引用, 无法在任何地方通过反射访问该类的方法(线5)

上述描述的情况中, 类加载器基本不会被回收, 除非类加载器加载的类全部被回收, 类加载器主要分为 引导类加载器 , 扩展类加载器, 系统类加载器,   引导类加载器负责加载系统类(String, Object等)基本不会被回收, 扩展类加载器负责加载指定的jar也不会被回收, 系统类加载器用来加载自己定义的类, 可能被回收

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值