深研JVM 探讨类加载过程

笔记 同时被 2 个专栏收录
36 篇文章 0 订阅
22 篇文章 0 订阅

类加载过程

我们都知道,编写的java代码是不能直接运行的,需要编译成class文件才行,这个编译需要通过javac命令就可以实现。然而生成的class文件是一个二进制文件,是不方便我们阅读的,我们需要通过javap命令进行反编译。
比如,我下面一段代码
在这里插入图片描述
反编译后是这样的内容:
在这里插入图片描述
我们分析一下反编译之后的结果
可以看到 一个class文件包含了几部分
第一部分:是类的描述信息,他记录了类的存储位置,最后一次修改时间,md5值,以及从哪个java类里编译出来的
第二部分:还是一些描述信息,主要描述了这个类是用什么样的jdk编译的,52表示jdk8
第三部分:常量池
第四部分:字段信息
第五部分:方法信息

//1.描述信息
Classfile /Users/weichenglong/Desktop/work/jvmTest/src/main/java/com/wcl/JVMTest.class
  Last modified 2021-10-18; size 649 bytes
  MD5 checksum e062633c7095f59d343f5f311df1413c
  Compiled from "JVMTest.java"
//2.描述信息
public class com.wcl.JVMTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
//3.常量池
Constant pool:
   #1 = Methodref          #12.#28        // java/lang/Object."<init>":()V
   #2 = Class              #29            // java/lang/StringBuilder
   #3 = Methodref          #2.#28         // java/lang/StringBuilder."<init>":()V
   #4 = Fieldref           #7.#30         // com/wcl/JVMTest.staticField:Ljava/lang/String;
   #5 = Methodref          #2.#31         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #6 = Fieldref           #7.#32         // com/wcl/JVMTest.field:Ljava/lang/String;
   #7 = Class              #33            // com/wcl/JVMTest
   #8 = String             #34            // AAA
   #9 = Methodref          #2.#35         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #7.#28         // com/wcl/JVMTest."<init>":()V
  #11 = Methodref          #7.#36         // com/wcl/JVMTest.add:()Ljava/lang/String;
  #12 = Class              #37            // java/lang/Object
  #13 = Utf8               CONST_FIELD
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               ConstantValue
  #16 = Utf8               staticField
  #17 = Utf8               field
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               add
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               SourceFile
  #27 = Utf8               JVMTest.java
  #28 = NameAndType        #18:#19        // "<init>":()V
  #29 = Utf8               java/lang/StringBuilder
  #30 = NameAndType        #16:#14        // staticField:Ljava/lang/String;
  #31 = NameAndType        #38:#39        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #17:#14        // field:Ljava/lang/String;
  #33 = Utf8               com/wcl/JVMTest
  #34 = Utf8               AAA
  #35 = NameAndType        #40:#23        // toString:()Ljava/lang/String;
  #36 = NameAndType        #22:#23        // add:()Ljava/lang/String;
  #37 = Utf8               java/lang/Object
  #38 = Utf8               append
  #39 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #40 = Utf8               toString
{
 //3.字段信息
  private static final java.lang.String CONST_FIELD;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String AAA

  private static java.lang.String staticField;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

  private java.lang.String field;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

//4.方法信息
  public com.wcl.JVMTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public java.lang.String add();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: getstatic     #4                  // Field staticField:Ljava/lang/String;
        10: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        13: aload_0
        14: getfield      #6                  // Field field:Ljava/lang/String;
        17: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: ldc           #8                  // String AAA
        22: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: areturn
      LineNumberTable:
        line 10: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #7                  // class com/wcl/JVMTest
         3: dup
         4: invokespecial #10                 // Method "<init>":()V
         7: invokevirtual #11                 // Method add:()Ljava/lang/String;
        10: pop
        11: return
      LineNumberTable:
        line 15: 0
        line 16: 11
}
SourceFile: "JVMTest.java"

加载

那么jvm是怎样加载class文件的呢?
当一个类被创建事例,或者被引用到的时候,如果虚拟机发现之前没有加载过这个类,会通过类加载器,即ClassLoader把类加载进内存,在加载的过程中,主要做了三件事:

  1. 读取类的二进制流
  2. 把二进制流转换成方法区的数据结构,并存到方法区
  3. 在java堆中产生一个java.lang.Class对象

链接

加载完成之后,就会进入连接的步骤,连接这个步骤又可以细分为验证,准备,解析三个部分

验证1

  • 作用:验证class文件是不是符合规范
  • 文件格式的验证(是否以0xCAFFBABE开头;版本号是否合理)
  • 元数据验证
    * 是否有父类
    * 是否继承了final类(final类不能被继承,如果继承了就有问题了)
    * 非抽象类实现了所有抽象方法
  • 字节码验证
    • 运行检查
    • 栈数据类型和操作码操作参数吻合(比如栈空间只有2字节,但其实需要大于2字节,此时就认为这个字节码是有问题的)
    • 跳转指令指向合理的位置
  • 符号引用验证
    • 常量池中描述类是否存在
    • 访问的方法或字段是否存在且有足够的权限
  • 可以使用-Xverify:none关闭验证

准备

如果验证通过,那么将会进入到准备的环节了
准备的作用是:

  • 为类的静态变量分配内存,初始化为操作系统的初始值
    • final static 修饰的变量:直接复制为用户定义的初始值,比如private final static int value = 123,直接赋值为123
    • private static int value = 123,该阶段的值依然是0

解析

准备完成之后就可以进入解析了,解析的作用是:把符号引用转换成直接引用

初始化

解析完成会后,就会进入初始化阶段,在这个阶段,jvm 会执行clinit方法,clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法

  • 初始化的顺序和源文件中的顺序一致
  • 子类的clinit被调用前,会先调用父类的clinit
  • JVM会保证clinit方法的线程安全性
  • 初始化时,如果实例化一个新对象,会调用init方法对实力变量进行初始化,并执行对应的构造方法内的代码。

看一下下面这个类,我编写代码里有静态块,构造块,构造方法以及main方法,那么他们的执行顺序会是怎么样子的呢?
在这里插入图片描述
可以看到,先执行了,静态代码块,然后执行了main方法,再执行了构造块,最后执行了构造方法。

使用

初始化完成这个类后,就可以使用该类了

卸载

当不使用这个类的时候就可以把他卸载掉

当然呢,该文中描述的这个类加载流程是一个比较常规的类加载流程,事实上类加载的时候并不一定完全按照这个流程去做,比方说,解析不一定要在初始化之前,也有可能在初始化之后去做解析

  • 2
    点赞
  • 2
    评论
  • 4
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值