JVM 类加载机制 详细学习笔记

本文详细解析了JVM的学习过程,包括类文件结构,通过实例探讨字节码执行流程、方法调用、多态、异常处理、finally和同步机制。深入剖析类加载阶段的五个步骤和双亲委派模式,适合初学者理解JVM工作原理。
摘要由CSDN通过智能技术生成

本篇博客记录个人学习jvm的过程,如有错误,敬请指正



一、类文件结构

由于类文件结构过于抽象,都是16进制的的数字组成,作者学习水平有限,暂时无法学习,可以参考下面视频
黑马jvm视频
比起看类文件结构,我们可以通过对.class文件进行反编译查看
javap -v 类名.class (对class文件进行反编译)
下面可以通过一个个简单的demo了解方法执行流程

二、字节码简单案例

1.案例1(基本执行流程)

public class Demo1 {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

下面是所展示的信息

Classfile /E:/javaTest/ThreadDemo1/out/production/ThreadDemo1/com/atguigu/test8/Demo1.class
  Last modified 2022711; size 613 bytes
  SHA-256 checksum 4307a5c9346bae1dfb88c6c681ac749b46970970618fed085b66bd1573f5f99b
  Compiled from "Demo1.java"
public class com.atguigu.test8.Demo1
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #6                          // com/atguigu/test8/Demo1
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // com/atguigu/test8/Demo1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/atguigu/test8/Demo1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo1.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               com/atguigu/test8/Demo1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public com.atguigu.test8.Demo1();
    descriptor: ()V
    flags: (0x0001) 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 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/atguigu/test8/Demo1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 15: 0
        line 16: 3
        line 17: 6
        line 18: 10
        line 19: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo1.java"

常量池载入运行时常量池(属于方法区)
在这里插入图片描述
字节码放入方法区

在这里插入图片描述注意:当数的大小超过short类型的最大值时,它会存储在常量池中
在这里插入图片描述

下面局部变量大小为4,操作数栈大小为2
在这里插入图片描述
下面开始执行方法区中的字节码,第一行是bipush
在这里插入图片描述

  • bipush :将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

下一行是istore 1,代表的是将操作数栈顶数据弹出,存入局部变量表的slot(槽位)1
在这里插入图片描述
ldc #3代表将运行时常量池中的#3 所指代的数据放入栈中
在这里插入图片描述

istore2代表将栈顶元素放入slot2槽位
在这里插入图片描述
iload1和iload2代表将槽位1和槽位2读取到操作数栈中
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

iadd将操作数栈中的两个元素弹出栈并相加,并加运算结果在压入操作数栈中

istore 3放入3号槽位
在这里插入图片描述
getstatic #4:在运行时常量池中找到#4,发现是一个对象;在堆内存中找到该对象,将其引用放入操作数栈中;
在这里插入图片描述
iload 3将3号槽位中的数读入到栈中
在这里插入图片描述
invokevirtual 5:

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 发现是方法后,由虚拟机分配一个新的栈帧
  • 传递参数,执行新栈帧中的字节码

在这里插入图片描述

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容
    return
    弹出 main 栈帧,程序结束

2.案例2(i = i++)

public class Test {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;  
            i++;
        }
        System.out.println(x); // 0
    }

对应的字节码文件

Code:
     stack=2, locals=3, args_size=1	// 操作数栈分配2个空间,局部变量表分配 3 个空间
        0: iconst_0	// 准备一个常数 0
        1: istore_1	// 将常数 0 放入局部变量表的 1 号槽位 i = 0
        2: iconst_0	// 准备一个常数 0
        3: istore_2	// 将常数 0 放入局部变量的 2 号槽位 x = 0	
        4: iload_1		// 将局部变量表 1 号槽位的数放入操作数栈中
        5: bipush        10	// 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
        7: if_icmpge     21	// 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
       10: iload_2		// 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0 
       11: iinc          2, 1	// 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值为 1 
       14: istore_2	//将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了0
       15: iinc          1, 1 // 1 号槽位的值自增 1 
       18: goto          4 // 跳转到第4条指令
       21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       24: iload_2
       25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       28: return

3.案例3(分析方法调用)

public class Demo1 {
    public Demo1() {

    }
    private void test1() {

    }
    private final void test2() {

    }
    public void test3() {

    }
    public static void test4() {

    }
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        demo1.test1(); //私有
        demo1.test2(); //final
        demo1.test3(); //公共
        Demo1.test4(); //静态
    }
}

再调用不同类型的方法时,虚拟机指令有所区别

  • 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令

  • 普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定

  • 静态方法在调用时使用invokestatic指令

注意:这里的invokespecial和invokestatic都是静态绑定,在字节码阶段就可以知道调用的是哪一个类的哪个方法,这个invokevirtual是公有的方法可能会出现重写,所以这个带public 的方法在字节码阶段是不知道这个方法具体是哪个类的,属于动态绑定

code:	 
		 stack=2, locals=2, args_size=1
		 0: new           #2                  // class classLoad/Demo5   这个new分为两个步骤:调用构造方法首先会在堆中分配一块空间,然后空间分配成功后会把这个对象的引用放入操作数栈 
         3: dup  //把刚刚创建的对象的引用地址复制一份,放到栈顶  ;为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”:()V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1						//出栈,把这个对象的引用(栈低的)存储到局部变量中
         8: aload_1
         9: invokespecial #4                  // Method test1:()V  私有方法
        12: aload_1
        13: invokespecial #5                  // Method test2:()V  final修饰的方法
        16: aload_1
        17: invokevirtual #6                  // Method test3:()V  public修饰的方法
        20: invokestatic  #7                  // Method test4:()V  静态方法
        23: return

4.案例4(多态)

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤

  • 先通过栈帧中对象的引用找到对象
  • 分析对象头,找到对象实际的 Class
  • Class 结构中有 vtable
  • 查询 vtable 找到方法的具体地址
  • 执行方法的字节码

5.案例5(异常处理)

public class Demo1 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (Exception e) {
            i = 20;
        }
    }
}

对应的字节码文件

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          12
        8: astore_2
        9: bipush        20
       11: istore_1
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/Exception
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )

6.案例6(finally)

public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (Exception e) {
			i = 20;
		} finally {
			i = 30;
		}
	}

对应字节码如下:

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        //try块
        2: bipush        10           //-----------try   try的范围可以从异常表中查询到
        4: istore_1
        //try块执行完后,会执行finally    
        5: bipush        30           //-----------fainal
        7: istore_1					  // 把30赋值给i,这个30的赋值是在finally代码块中的
        8: goto          27
       //catch块     
       11: astore_2 //把异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       //catch块执行完后,会执行finally          
       15: bipush        30          //-----------fainal
       17: istore_1
       18: goto          27
       //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
       21: astore_3
       22: bipush        30         //-----------fainal
       24: istore_1
       25: aload_3  //找到刚刚没有名字的异常
       26: athrow  //抛出这个没有名字的异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

通过上面的字节码可知,其实是分为三个分支,try,catch,还有catch未捕获到的异常,这三个分支后面其实都有一份finally

7.案例7(finally中return)

public class Demo1 {
    public static void main(String[] args) {
        int i = Demo1.test();
        // 结果为 20
        System.out.println(i);
    }

    public static int test() {
        int i;
        try {
            i = 10;
            return i;
        } finally {
            i = 20;
            return i;
        }
    }
}

字节码如下:

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10   //放入栈顶
        2: istore_0      //slot 0  (从栈顶移除了)
        3: iload_0
        4: istore_1  // 暂存返回值
        5: bipush        20
        7: istore_0    //20这个值对10进行了覆盖
        8: iload_0
        9: ireturn	// ireturn 会【返回操作数栈顶】的整型值 20
       // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
       10: astore_2
       11: bipush        20
       13: istore_0
       14: iload_0
       15: ireturn	// 这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常
     Exception table:
        from    to  target type
            0     5    10   any
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常

我们将finally中的return去除:

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10  //把10放入栈顶
        2: istore_0 // 把10存储在局部变量表的0号槽位
        3: iload_0	// 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
        4: istore_1 // 加载到局部变量表的1号位置,【目的是为了固定返回值】
        5: bipush        20  //------执行finally代码块
        7: istore_0 // 赋值给i 20
        8: iload_1 // 【加载局部变量表1号位置的数10到操作数栈】
        9: ireturn // 返回操作数栈顶元素 10
       10: astore_2 //异常发生:
       11: bipush        20
       13: istore_0
       14: aload_2 // 加载异常
       15: athrow // 仍然会抛出异常
     Exception table:
        from    to  target type
            3     5    10   any

8.案例8(Synchronized)

public class Demo {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

对应的字节码如下

 Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup								  //复制对象的引用,栈顶会消耗一份该对象的引用
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1	//第二份对象的引用赋值给局部变量表中的lock,从这里结束,第一行代码执行完毕
         8: aload_1     //开始进入synchronized代码快,先把对象加载到操作数栈                    
         9: dup			//对这个lock对象的引用进行复制,分别对应加锁和解锁两个阶段使用
        10: astore_2	//把刚刚复制出来的对象引用给存储起来到二号槽位(二号槽位是没有name的)
        11: monitorenter //这个指令会把栈顶的对象引用给消耗掉,对lock引用所执行的对象进行加锁操作
        12: getstatic     #3   // Field  java/lang/System.out:Ljava/io/PrintStream;开始执行打印方法
        15: ldc           #4                  // String ok  ldc表示加载常量
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2   //加载刚刚暂存在局部变量表的lock对象引用
        21: monitorexit  //完成解锁
        22: goto          30
        //如果出现异常:
        25: astore_3   //把异常对象(e)的引用存储到局部变量表中的3号槽位
        26: aload_2    //加载刚刚暂存在局部变量表的lock对象引用
        27: monitorexit  //完成解锁
        28: aload_3    //把刚刚那个异常对象从局部变量表中加载到操作数栈中,进行抛出
        29: athrow
        30: return
      Exception table://如果12-22发生异常,那么就会进入到25行指令,是为了保证进入到异常后还能正常解锁
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 12
        line 14: 20
        line 15: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

三、类加载阶段

类加载主要分为以下阶段
在这里插入图片描述

1.加载

类加载过程的第一步,主要完成下面 3 件事情:

  • 通过全类名获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

如果这个类还有父类没有加载,先加载父类

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了

2.验证

在这里插入图片描述

3.准备

为 static 变量分配空间(jdk8后这个静态变量在堆中),设置默认值

定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字 public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。

4.解析

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

符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量

符号引用仅仅就是一个符号引用,jvm不知道它的具体含义是什么,但是经过实际解析后jvm就可以知道这个类,方法在内存中实实在在的位置了

5.初始化

初始化阶段是执行初始化方法 clinit()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。虚拟机会保证这个类的的线程安全

对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

1、当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。

  • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
  • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
  • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
  • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。

2、使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,需要触发其初始化。

3、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

4、main方法所在的类,虚拟机会先初始化这个类。

5、MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。

三、类加载阶器

所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存 注意:数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

名称加载的类说明
Bootstrap ClassLoader(启动类加载器)JAVA_HOME/jre/lib无法直接访问
Extension ClassLoader(扩展类加载器)JAVA_HOME/jre/lib/ext上级为Bootstrap
Application ClassLoader(应用程序类加载器)classpath上级为Extension
自定义类加载器自定义上级为Application

双亲委派模式

双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则

  • 类加载器收到了类加载请求,它会首先委托上级类加载器去执行
  • 如果上一级加载器还存在上级,那么会继续请求,直到达到启动类加载器
  • 如果父类加载器可以完成类加载任务,那么直接返回,否则委派给下级
    在这里插入图片描述
    loadClass源码:
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.首先查找本类是否已经被该类加载器加载过了
        Class<?> c = findLoadedClass(name);
        // 如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 看是否被它的上级加载器加载过了 Extension 的上级是Bootstarp,但它显示为null
                if (parent != null) {
                    //2.有上级的话就会委派上级 loadclass
                    c = parent.loadClass(name, false);
                } else {
                    // 3.看是否被启动类加载器加载过
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }
 
            if (c == null) {
                // 如果还是没有找到,先让拓展类加载器调用 findClass 方法去找到该类,如果还是没找到,就抛出异常
                // 然后让应用类加载器去找 classpath 下找该类
                long t1 = System.nanoTime();
                c = findClass(name);
 
                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

自定义类加载器
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。想打破双亲委派机制的话需要重写loadClass() 方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值