前言
春招结束,即将步入职场。这是我为以后职场发展所写的第一篇文章,从JVM底层开始,边学习边记录。加油!
一、java执行某个类代码的大致流程
我们能够使用IDE很轻松地编写出一个java类文件,也可以很轻松地执行类文件并得到结果。
但在底层,JVM其实做了很多事情,java执行某个类的代码大致流程如下:
- jvm.dll是C++的,类似于java的jar包,毕竟JVM是用C写的。
- 这里流程图面有很多东西,先放这占坑,后面学一个填一个。
二、loadClass流程
上图大致列出了Java执行类代码的流程,这里面的内容有很多,其中classLoader.loadClass(“com.jim.jvm.ClassLoadTest”)
这个步骤就包括以下5个步骤:
-
加载:将JVM翻译后的字节码文件ClassLoadTest.class从磁盘载入内存,而在这之前,会进行下面的几个步骤;
-
验证:Java字节码文件有自己的规则,因此对于即将解释执行的字节码文件,需要对其进行校验,以保证后续流程的正确性;
-
准备:为类中的静态变量分配内存,并赋予默认值。这里的默认值不是Java文件中定义的初始值,而是该数据类型对应的默认初始值。比如Java类中定义了一个静态的int型变量,并设置初值为10,那么在准备阶段为其设定的初值是0,而不是10;
-
解析:进行静态链接,将符号引用(类中的静态方法,如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的代码,而动态链接能够很好地解决这个问题。
- 初始化:此时会真正地初始化那些静态变量,同时会运行类中的静态代码块。
结语
本篇文章是基于类加载的流程来理解的,后面会尝试进入源码来进行理解。