(1)JVM 类加载之流程理解

本文探讨了Java执行类代码的底层流程,包括加载、验证、准备、解析和初始化五个步骤。在加载阶段,JVM将字节码文件载入内存。验证确保字节码的正确性,准备阶段为静态变量分配内存并赋默认值。解析过程将符号引用转换为直接引用,静态链接提高类加载速度,而动态链接适应多态场景。初始化则涉及静态变量的实际初始化和静态代码块的执行。

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


前言

春招结束,即将步入职场。这是我为以后职场发展所写的第一篇文章,从JVM底层开始,边学习边记录。加油!

一、java执行某个类代码的大致流程

我们能够使用IDE很轻松地编写出一个java类文件,也可以很轻松地执行类文件并得到结果。
但在底层,JVM其实做了很多事情,java执行某个类的代码大致流程如下:

java执行一个类大致流程

  1. jvm.dll是C++的,类似于java的jar包,毕竟JVM是用C写的。
  2. 这里流程图面有很多东西,先放这占坑,后面学一个填一个。

二、loadClass流程

上图大致列出了Java执行类代码的流程,这里面的内容有很多,其中classLoader.loadClass(“com.jim.jvm.ClassLoadTest”)
这个步骤就包括以下5个步骤:

类加载流程图

  1. 加载:将JVM翻译后的字节码文件ClassLoadTest.class从磁盘载入内存,而在这之前,会进行下面的几个步骤

  2. 验证:Java字节码文件有自己的规则,因此对于即将解释执行的字节码文件,需要对其进行校验,以保证后续流程的正确性;

  3. 准备:为类中的静态变量分配内存,并赋予默认值。这里的默认值不是Java文件中定义的初始值,而是该数据类型对应的默认初始值。比如Java类中定义了一个静态的int型变量,并设置初值为10,那么在准备阶段为其设定的初值是0,而不是10;

  4. 解析:进行静态链接,将符号引用(类中的静态方法,如main())替换成直接引用(指针:指向该静态方法在内存方法区中的地址),顾名思义,我们可以将类中的一个个静态方法名当作一个个的符号,而这些静态方法被加载到内存的方法区后会有其自身的地址,解析过程就是将这一个个的符号替换成指向方法在内存中地址的指针,这样在执行代码的时候就可以通过这些指针找到代码在内存中的地址,从而能够运行;与静态链接相对应的还有种动态链接,这个是在程序运行时进行的替换操作,不会在解析阶段进行;

为什么要区分静态链接动态链接呢?
比如对于以下的代码:

package com.jim.jvm.classload;

public class Add {

   private int add(int a, int b){
       return a + b;
   }

   public static void main(String[] args) {
       int a = 10, b = 20;
       Add ad = new Add();
       int result = ad.add(a, b);
   }
}

使用JVM将其编译成字节码文件后得到Add.class,在Terminal运行:

javap -v Add.class

会得到一个含义上等价于字节码的结果(JVM肯定不会按照这个去执行代码),这些更可读:

Classfile /D:/idea_projects_2019.2.4/jvm/out/production/jvm/com/jim/jvm/classload/Add.class
  Last modified 2021-5-8; size 605 bytes
  MD5 checksum 23e3271ef989c6e8a2418ed14c8ea71c
  Compiled from "Add.java"
public class com.jim.jvm.classload.Add
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#26         // java/lang/Object."<init>":()V
   #2 = Class              #27            // com/jim/jvm/classload/Add
   #3 = Methodref          #2.#26         // com/jim/jvm/classload/Add."<init>":()V
   #4 = Methodref          #2.#28         // com/jim/jvm/classload/Add.add:(II)I
   #5 = Class              #29            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/jim/jvm/classload/Add;
  #13 = Utf8               add
  #14 = Utf8               (II)I
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               b
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               ad
  #23 = Utf8               result
  #24 = Utf8               SourceFile
  #25 = Utf8               Add.java
  #26 = NameAndType        #6:#7          // "<init>":()V
  #27 = Utf8               com/jim/jvm/classload/Add
  #28 = NameAndType        #13:#14        // add:(II)I
  #29 = Utf8               java/lang/Object
{
  public com.jim.jvm.classload.Add();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jim/jvm/classload/Add;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: new           #2                  // class com/jim/jvm/classload/Add
         9: dup
        10: invokespecial #3                  // Method "<init>":()V
        13: astore_3
        14: aload_3
        15: iload_1
        16: iload_2
        17: invokespecial #4                  // Method add:(II)I
        20: istore        4
        22: return
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 14
        line 13: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            3      20     1     a   I
            6      17     2     b   I
           14       9     3    ad   Lcom/jim/jvm/classload/Add;
           22       1     4 result   I
}
SourceFile: "Add.java"

这一堆乱七八糟的东西可以看作是JVM的汇编指令。
结果中有个Constant pool,也就是常量池。
仔细观察会发现,有一行:

6: new    #2    // class com/jim/jvm/classload/Add

"//"后面其实已经对其进行解释了,这是在创建一个Add对象
那么这个 “#2” 是什么东西呢?
再仔细观察,会发现有个Constant pool,也就是常量池。
往下找会发现:

#2 = Class          #27        // com/jim/jvm/classload/Add

“=Class” 说明 “#2” 指一个类,后面的注释也进行了说明。
以上操作对应于java代码的:

new Add();

再比如,java代码中执行了:

ad.add(a, b)

对应以上“字节码”中的:

17: invokespecial   #4             // Method add:(II)I
#4 = Methodref      #2.#28         // com/jim/jvm/classload/Add.add:(II)I
#2 = Class          #27            // com/jim/jvm/classload/Add
#27 = Utf8               com/jim/jvm/classload/Add
#28 = NameAndType   #13:#14        // add:(II)I
#13 = Utf8               add
#14 = Utf8               (II)I

会发现这就是一个个符号替换的过程,也就是

"#4" -> "#2.#28" -> "#27.#28" -> "Add.#28" -> "Add.#13:#14" -> "Add.add:#14" -> "Add.add:(II)I"

在将java文件编译成字节码文件后,可以看见,这些符号放在了Constant pool中,是静态的。当加载字节码文件时,其中的静态符号常量(如main()等),在运行时的内存地址几乎不会变,此时就可以将其替换成一个个指向内存方法区中相应地址的指针,而对于非静态符号常量(需要动态链接的),只有在程序运行到这行代码时,这些符号常量才会被解析替换成指向代码内存地址的指针。
一方面,这可以提升类加载的速度;另一方面,例如面对多态的情况,比如有两种add函数,在程序运行到相应代码之前,JVM也不知道到底要运行哪一种add的代码,而动态链接能够很好地解决这个问题。

  1. 初始化:此时会真正地初始化那些静态变量,同时会运行类中的静态代码块。

结语

本篇文章是基于类加载的流程来理解的,后面会尝试进入源码来进行理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

starslightshine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值