今天这篇文章我们具体分析一下java程序运行起来之中所包含的具体细节,通过这些细节带我们窥探JVM实现的原理。
一、前端编译
使用javac命令将.java程序编译为JVM可解析的.class字节码文件,java作为一种解释型编程语言(当然这么说带有片面性,jvm自带的即时编译器同样可以将字节码直接编译为机器码运行,但是这里我们就不分析即时编译器是如何运行的了),负责将字节码翻译为机器码运行,所以对于字节码内容的理解对了解JVM的原理非常关键。
二、class字节码
可以通过javap -v classfile将class字节码文件翻译为人类可以看懂的形式显示。字节码文件包含了魔数,jdk版本号,常量池,方法指令信息等关于类信息的所有数据,其中常量池会在此类被加载的时候加载到方法区的运行时常量池中
三、类的加载
类加载器找到类的字节码文件,将字节码中类的元数据(接口,类,方法信息等)存储到JVM方法区中,将常量池存储到方法区中的运行时常量池中,并在堆中创建一个Class<T>对象。运行时常量池中的符号引用将会在类加载的解析阶段转换为直接引用,直接执行此类中的方法、类变量、成员变量所在的内存地址。
第二幅图应该为类加载过程中的初始化时机
下面根据一段代码,直观的分析一下具体细节,下面有一段代码
public class JVM {
//基本数据类型 int类型-127-128默认是在常量池中
private static final int N1 = 2019;
private static int N2 = 2020;
private final int N3 = 2021;
private int N4 = 2022;
//包装类型
private static final Integer T1 = 2019;
private static Integer T2 = 2020;
private final Integer T3 = 2021;
private Integer T4 = 2022;
//引用类型
private static final Demo demo1 = new Demo();
private static Demo demo2 = new Demo();
private final Demo demo3 = new Demo();
private Demo demo4 = new Demo();
public static void main(String[] args) {
testDemo();
final int aaa = 8080;
int bbb = 9090;
System.out.println(aaa);
System.out.println(bbb);
}
public static void testDemo() {
Demo tempDemo = new Demo();
tempDemo.test();
}
}
使用javap -v查看编译后的字节码如下
Classfile /C:/Users/sky/Desktop/JVM.class
Last modified 2019-11-5; size 1112 bytes
MD5 checksum 8d2f6171cd7dcdd87e97078a1572e1b5
Compiled from "JVM.java"
public class JVM
SourceFile: "JVM.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
//此部分为常量池信息,类加载时加载到方法区中的运行时常量池内
//常量池包含了被final修饰的基本数据类型的字面量2019,2021和字符串字面量
//包含了方法,字段的字符引用,在类加载的解析阶段转换为直接指向其内存地址的直接引用
Constant pool:
#1 = Methodref #20.#49 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#50 // JVM.N3:I
#3 = Fieldref #19.#51 // JVM.N4:I
#4 = Methodref #52.#53 // java/lang/Integer.valueOf:(I)Ljava
/lang/Integer;
#5 = Fieldref #19.#54 // JVM.T3:Ljava/lang/Integer;
#6 = Fieldref #19.#55 // JVM.T4:Ljava/lang/Integer;
#7 = Class #56 // Demo
#8 = Methodref #7.#49 // Demo."<init>":()V
#9 = Fieldref #19.#57 // JVM.demo3:LDemo;
#10 = Fieldref #19.#58 // JVM.demo4:LDemo;
#11 = Fieldref #59.#60 // java/lang/System.out:Ljava/io/Prin
tStream;
#12 = Methodref #61.#62 // java/io/PrintStream.println:(I)V
#13 = Methodref #7.#63 // Demo.test:()V
#14 = Fieldref #19.#64 // JVM.N2:I
#15 = Fieldref #19.#65 // JVM.T1:Ljava/lang/Integer;
#16 = Fieldref #19.#66 // JVM.T2:Ljava/lang/Integer;
#17 = Fieldref #19.#67 // JVM.demo1:LDemo;
#18 = Fieldref #19.#68 // JVM.demo2:LDemo;
#19 = Class #69 // JVM
#20 = Class #70 // java/lang/Object
#21 = Utf8 N1
#22 = Utf8 I
#23 = Utf8 ConstantValue
#24 = Integer 2019
#25 = Utf8 N2
#26 = Utf8 N3
#27 = Integer 2021
#28 = Utf8 N4
#29 = Utf8 T1
#30 = Utf8 Ljava/lang/Integer;
#31 = Utf8 T2
#32 = Utf8 T3
#33 = Utf8 T4
#34 = Utf8 demo1
#35 = Utf8 LDemo;
#36 = Utf8 demo2
#37 = Utf8 demo3
#38 = Utf8 demo4
#39 = Utf8 <init>
#40 = Utf8 ()V
#41 = Utf8 Code
#42 = Utf8 LineNumberTable
#43 = Utf8 main
#44 = Utf8 ([Ljava/lang/String;)V
#45 = Utf8 testDemo
#46 = Utf8 <clinit>
#47 = Utf8 SourceFile
#48 = Utf8 JVM.java
#49 = NameAndType #39:#40 // "<init>":()V
#50 = NameAndType #26:#22 // N3:I
#51 = NameAndType #28:#22 // N4:I
#52 = Class #71 // java/lang/Integer
#53 = NameAndType #72:#73 // valueOf:(I)Ljava/lang/Integer;
#54 = NameAndType #32:#30 // T3:Ljava/lang/Integer;
#55 = NameAndType #33:#30 // T4:Ljava/lang/Integer;
#56 = Utf8 Demo
#57 = NameAndType #37:#35 // demo3:LDemo;
#58 = NameAndType #38:#35 // demo4:LDemo;
#59 = Class #74 // java/lang/System
#60 = NameAndType #75:#76 // out:Ljava/io/PrintStream;
#61 = Class #77 // java/io/PrintStream
#62 = NameAndType #78:#79 // println:(I)V
#63 = NameAndType #80:#40 // test:()V
#64 = NameAndType #25:#22 // N2:I
#65 = NameAndType #29:#30 // T1:Ljava/lang/Integer;
#66 = NameAndType #31:#30 // T2:Ljava/lang/Integer;
#67 = NameAndType #34:#35 // demo1:LDemo;
#68 = NameAndType #36:#35 // demo2:LDemo;
#69 = Utf8 JVM
#70 = Utf8 java/lang/Object
#71 = Utf8 java/lang/Integer
#72 = Utf8 valueOf
#73 = Utf8 (I)Ljava/lang/Integer;
#74 = Utf8 java/lang/System
#75 = Utf8 out
#76 = Utf8 Ljava/io/PrintStream;
#77 = Utf8 java/io/PrintStream
#78 = Utf8 println
#79 = Utf8 (I)V
#80 = Utf8 test
{
public JVM();
flags: ACC_PUBLIC
Code:
//对象构造方法 会为成员变量赋值
//stack=3 操作数栈深为3, locals=1 局部变量个数, args_size=1 参数个数
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
":()V
4: aload_0
5: sipush 2021
8: putfield #2 // Field N3:I
11: aload_0
12: sipush 2022
15: putfield #3 // Field N4:I
18: aload_0
19: sipush 2021
22: invokestatic #4 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
25: putfield #5 // Field T3:Ljava/lang/Integer;
28: aload_0
29: sipush 2022
32: invokestatic #4 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
35: putfield #6 // Field T4:Ljava/lang/Integer;
38: aload_0
39: new #7 // class Demo
42: dup
43: invokespecial #8 // Method Demo."<init>":()V
46: putfield #9 // Field demo3:LDemo;
49: aload_0
50: new #7 // class Demo
53: dup
54: invokespecial #8 // Method Demo."<init>":()V
57: putfield #10 // Field demo4:LDemo;
60: return
LineNumberTable:
line 1: 0
line 5: 4
line 6: 11
line 11: 18
line 12: 28
line 17: 38
line 18: 49
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: sipush 9090
3: istore_2
4: getstatic #11 // Field java/lang/System.out:Ljav
a/io/PrintStream;
7: sipush 8080
10: invokevirtual #12 // Method java/io/PrintStream.prin
tln:(I)V
13: getstatic #11 // Field java/lang/System.out:Ljav
a/io/PrintStream;
16: iload_2
17: invokevirtual #12 // Method java/io/PrintStream.prin
tln:(I)V
20: return
LineNumberTable:
line 22: 0
line 23: 4
line 24: 13
line 25: 20
public static void testDemo();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #7 // class Demo
3: dup
4: invokespecial #8 // Method Demo."<init>":()V
7: astore_0
8: aload_0
9: invokevirtual #13 // Method Demo.test:()V
12: return
LineNumberTable:
line 28: 0
line 29: 8
line 30: 12
static {}; //所有的没有被final修饰的或者被final修饰的引用类型的类变量被赋值,会在类加载的初始
//化阶段执行
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: sipush 2020
3: putstatic #14 // Field N2:I
6: sipush 2019
9: invokestatic #4 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
12: putstatic #15 // Field T1:Ljava/lang/Integer;
15: sipush 2020
18: invokestatic #4 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
21: putstatic #16 // Field T2:Ljava/lang/Integer;
24: new #7 // class Demo
27: dup
28: invokespecial #8 // Method Demo."<init>":()V
31: putstatic #17 // Field demo1:LDemo;
34: new #7 // class Demo
37: dup
38: invokespecial #8 // Method Demo."<init>":()V
41: putstatic #18 // Field demo2:LDemo;
44: return
LineNumberTable:
line 4: 0
line 9: 6
line 10: 15
line 15: 24
line 16: 34
}
对于上面代码运行的总结:
1.变量的初始化
- 类变量(被static修饰的变量)和同时被static和final修饰的引用类型会在类构造方法<cinit>()中中初始化,初始化操作是在类加载过程中的初始化阶段,static代码块也是在类构造方法<cinit>()中执行
- 同时被static和final修饰的基础数据类型和string类型会在类加载过程中的准备阶段使用常量池中的值对其初始化
- 非静态成员变量(无static修饰)会在对象构造方法中初始化
2.方法的执行
1类加载器将JVM.class加载到方法区,并且会将运行时常量池中的符号引用转换为直接引用
2通过main方法的直接引用找到了main方法内存地址,jvm可以解析main方法的字节码指令来执行方法之中的操作
3通过运行时常量池找到testDemo方法的直接引用,执行testDemo的字节码指令
4.查看Demo类有没有在运行时常量池中加载,若没有加载Demo.class字节码,将字节码加载到运行时常量区,并将Demo类 的运行时常量池中的符号引用转化为直接引用
5将JVM类的常量池中的Demo类的符号引用转换为直接引用,这样可以直接找到Demo类了
6在堆中给demo对象分配空间,并且demo对象中持有Demo类的运行时常量池
7使用在demo对象中常量池引用找到Demo类常量池,在常量池中找到test()方法的直接应用,执行test方法的字节码指令