[java基本功系列]jvm之不需要死记硬背的jvm基本原理

1,872 阅读30分钟

前言

对基本原理的了解,动手是最好的;

哪里入手

例子

 1package com.java.study.jvm;
2
3/**
4 * @author zhangpeng
5 * @since 2020/1/15 3:33 下午
6 */

7public class JvmHello {
8    public static final int i = 2020;
9
10    public static void main(String[] args) {
11        JvmHello jvmHello = new JvmHello();
12        int a = 1;
13        int b = 2;
14        int c = jvmHello.calculate1(a, b);
15        int d = jvmHello.calculate2(a, b);
16    }
17
18    private int calculate2(int a, int b) {
19        int x = 666;
20        return x / (a + b);
21    }
22
23    private int calculate1(int a, int b) {
24        return (a + b) * 2333;
25    }
26}

这段代码我就不解释了 直接编译字节码搞起

1# 编译生成 JvmHello.class文件
2javac JvmHello.java
3# 反编译字节码内容
4javap -verbose -p JvmHello.class

记得之前书里提到的,编译一次到处执行,那么首先文件要被加载进来,运行在一个环境里面;所以我们有了初步的图

image-20200115172146495
image-20200115172146495

JvmHello.java -> JvmHello.class -> 类装载系统加载进来 -> 在虚拟机环境执行

接着我们看下JVMHello.class的内容

  1Classfile /Users/zhangpeng/workspacke/mytest/study/src/main/java/com/java/study/jvm/JvmHello.class
2  Last modified 2020-1-15; size 530 bytes
3  MD5 checksum d1725552383bf6c86a00f1517d2b4c51
4  Compiled from "JvmHello.java"
5public class com.java.study.jvm.JvmHello
6  minor version: 0
7  major version: 52
8  flagsACC_PUBLICACC_SUPER
9Constant pool:
10   #1 
= Methodref          #6.#22         // java/lang/Object."<init>":()V
11   #2 = Class              #23            // com/java/study/jvm/JvmHello
12   #3 = Methodref          #2.#22         // com/java/study/jvm/JvmHello."<init>":()V
13   #4 = Methodref          #2.#24         // com/java/study/jvm/JvmHello.calculate1:(II)I
14   #5 = Methodref          #2.#25         // com/java/study/jvm/JvmHello.calculate2:(II)I
15   #6 = Class              #26            // java/lang/Object
16   #7 = Utf8               i
17   #8 = Utf8               I
18   #9 = Utf8               ConstantValue
19  #10 = Integer            2020
20  #11 = Utf8               <init>
21  #12 = Utf8               ()V
22  #13 = Utf8               Code
23  #14 = Utf8               LineNumberTable
24  #15 = Utf8               main
25  #16 = Utf8               ([Ljava/lang/String;)V
26  #17 = Utf8               calculate2
27  #18 = Utf8               (II)I
28  #19 = Utf8               calculate1
29  #20 = Utf8               SourceFile
30  #21 = Utf8               JvmHello.java
31  #22 = NameAndType        #11:#12        // "<init>":()V
32  #23 = Utf8               com/java/study/jvm/JvmHello
33  #24 = NameAndType        #19:#18        // calculate1:(II)I
34  #25 = NameAndType        #17:#18        // calculate2:(II)I
35  #26 = Utf8               java/lang/Object
36{
37  public static final int i;
38    descriptor: I
39    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
40    ConstantValue: int 2020
41
42  public com.java.study.jvm.JvmHello();
43    descriptor: ()V
44    flags: ACC_PUBLIC
45    Code:
46      stack=1, locals=1, args_size=1
47         0: aload_0
48         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
49         4return
50      LineNumberTable:
51        line 70
52
53  public static void main(java.lang.String[]);
54    descriptor: ([Ljava/lang/String;)V
55    flags: ACC_PUBLIC, ACC_STATIC
56    Code:
57      stack=3, locals=6, args_size=1
58         0new           #2                  // class com/java/study/jvm/JvmHello
59         3: dup
60         4: invokespecial #3                  // Method "<init>":()V
61         7: astore_1
62         8: iconst_1
63         9: istore_2
64        10: iconst_2
65        11: istore_3
66        12: aload_1
67        13: iload_2
68        14: iload_3
69        15: invokespecial #4                  // Method calculate1:(II)I
70        18: istore        4
71        20: aload_1
72        21: iload_2
73        22: iload_3
74        23: invokespecial #5                  // Method calculate2:(II)I
75        26: istore        5
76        28return
77      LineNumberTable:
78        line 110
79        line 128
80        line 1310
81        line 1412
82        line 1520
83        line 1628
84
85  private int calculate2(intint);
86    descriptor: (II)I
87    flags: ACC_PRIVATE
88    Code:
89      stack=3, locals=4, args_size=3
90         0: sipush        666
91         3: istore_3
92         4: iload_3
93         5: iload_1
94         6: iload_2
95         7: iadd
96         8: idiv
97         9: ireturn
98      LineNumberTable:
99        line 190
100        line 204
101
102  private int calculate1(intint);
103    descriptor: (II)I
104    flags: ACC_PRIVATE
105    Code:
106      stack=2, locals=3, args_size=3
107         0: iload_1
108         1: iload_2
109         2: iadd
110         3: sipush        2333
111         6: imul
112         7: ireturn
113      LineNumberTable:
114        line 240
115}
116SourceFile: "JvmHello.java"

字节码分析

1-8行

描述了类的基本信息

  • 它是由哪个 *.java 文件编译而成的
  • 最后编译时间
  • 编译后的大小
  • MD5校验值
  • 遵循的java版本
  • 访问标识,ACC_PUBLIC字面意思公有的嘛;ACC_SUPER不清楚是什么,但是应该和super方法有关系

9-35行 Constant pool

运行时常量池

我们先分析下第一个常量,位于JVMHello.class第10行,我们会发现后面有关联项 一起放进来

1   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
2   #6 = Class              #26            // java/lang/Object
3  #11 = Utf8               <init>
4  #12 = Utf8               ()V
5  #22 = NameAndType        #11:#12        // "<init>":()V
6  #26 = Utf8               java/lang/Object

Methodref表示方法定义,右侧的注释内容(表示是由这几行组合起来的)

1java/lang/Object."<init>":()V

这段可以理解为该类的实例父类构造器的声明,此处也说明了JvmHello类的直接父类是Object.该方法默认返回值是V,也就是void,无返回值

同理分析下第二个常量,位于JVMHello.class第12行

1   #2 = Class              #23            // com/java/study/jvm/JvmHello
2   #3 = Methodref          #2.#22         // com/java/study/jvm/JvmHello."<init>":()V
3  #11 = Utf8               <init>
4  #12 = Utf8               ()V
5  #22 = NameAndType        #11:#12        // "<init>":()V
6  #23 = Utf8               com/java/study/jvm/JvmHello

这里描述的是默认的构造器JvmHello(),因为后面在main()方法里面new了对象 所以这里会初始化到常量池

同理分析下第三个常量,位于JVMHello.class第13行

1   #2 = Class              #23            // com/java/study/jvm/JvmHello   
2   #4 = Methodref          #2.#24         // com/java/study/jvm/JvmHello.calculate1:(II)I
3  #18 = Utf8               (II)I
4  #19 = Utf8               calculate1
5  #24 = NameAndType        #19:#18        // calculate1:(II)I

这里描述的是JvmHello类里面calculate1方法的定义

1    com/java/study/jvm/JvmHello.calculate1:(II)I

(II) 表示入参为两个基本类型int

(II)I 右边的这个I表示返回值也是基本类型int

连起来说就是 calculate1方法入参是两个int,返回值是int

那么同理可得 位于JVMHello.class第14行的变量表示的是 calculate2方法入参也是两个int,返回值也是int

上述就是运行时常量池信息的分析,常量池用于存放编译期生成的各种字面量和符号引用,常量池是被划分在了方法区这个里面,方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据到这我们补充一下我们的jvm图

image-20200116164521074
image-20200116164521074

36-115行 类内部方法描述

方法表集合

36-41行 静态常量i的定义

先看下静态常量的定义,位于JVMHello.class

1  public static final int i;
2  descriptor: I
3  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
4  ConstantValue: int 2020
  • 声明了一个公有变量i,类型为int
  • 返回值为int
  • 访问标识公共的、静态的、最终的
  • 常量值为2020

42-52行 类的构造器定义

 1  public com.java.study.jvm.JvmHello();
2    descriptor: ()V
3    flags: ACC_PUBLIC
4    Code:
5      stack=1, locals=1, args_size=1
6         0: aload_0
7         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
8         4return
9      LineNumberTable:
10        line 70
  • ()V 参考之前方法的定义描述 这里是空参的方法;V表示特殊类型void无返回值
  • ACC_PUBLIC 访问标识公共的
  • stack 最大操作数栈 JVM会根据这个值来分配帧栈的操作栈深度,这里是1
  • locals 局部变量所需存储空间,单位Slot,1Slot=4B,那么这里就是4个字节
  • args_size方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
  • aload_0 当中的0正是局部变量表里的Slot 0的含义。意思是将局部变量表里的Slot 0的东西压入操作数栈,这个Slot 0里的东西name正是this,也就是JvmHello的实例
  • invokespecial #1 invokespecial表示根据编译时类型来调用实例方法 #1表示执行 常量池里面定义的实例方法,即JvmHello();
  • return 从方法中返回,返回值为void
  • LineNumberTable 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系

这里我们产生了另一个概念,方法执行会进行压栈出栈

53-84行

main方法分析

 1  public static void main(java.lang.String[]);// main方法
2    descriptor: ([Ljava/lang/String;)V        // 入参String[],出参V(void)
3    flags: ACC_PUBLIC, ACC_STATIC                            // 公共的、静态的
4    Code:
5      stack=3, locals=6, args_size=1                    // 操作数栈3,局部变量6 Slot,参数个数为1
6         0new           #2                  // class com/java/study/jvm/JvmHello    new对象
7         3: dup                                                                // 复制栈顶部一个字长内容
8         4: invokespecial #3                  // Method "<init>":()V        执行JvmHello构造器
9         7: astore_1                                                    // 将returnAddress类型(引用类型)存入到局部变量[1]
10         8: iconst_1                                                    // 将int类型常量[1]压入到操作数栈
11         9: istore_2                                                    // 将int类型值存入局部变量[2]
12        10: iconst_2                                                    // 将int类型常量[2]压入到操作数栈
13        11: istore_3                                                    // 将int类型值存入局部变量[3]
14        12: aload_1                                                        // 从局部变量[1]中装载引用类型值
15        13: iload_2                                                        // 从局部变量[2]中装载int类型值 
16        14: iload_3                                                        // 从局部变量[3]中装载int类型值 
17        15: invokespecial #4                  // Method calculate1:(II)I      执行calculate1方法
18        18: istore        4                                        // 将int类型值存入局部变量[4]
19        20: aload_1                                                        // 从局部变量[1]中装载引用类型值
20        21: iload_2                                                        // 从局部变量[2]中装载int类型值 
21        22: iload_3                                                        // 从局部变量[3]中装载int类型值 
22        23: invokespecial #5                  // Method calculate2:(II)I            执行calculate2方法
23        26: istore        5                                        // 将int类型值存入局部变量[5]
24        28return                                                        // void返回
25      LineNumberTable:                                                
26        line 110
27        line 128
28        line 1310
29        line 1412
30        line 1520
31        line 1628

从第一行new对象说起

1  JvmHello jvmHello = new JvmHello();
2  // 这里的jvmHello就是局部变量[1];

那么new出来的对象放在哪里的,看过jvm相关内容的同学都知道对象是分配在里面的

关于的定义

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换 优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

接着看下面两行

1 int a = 1;
2 int b = 2;
3 // 1.这里先将常量[1] = 1压入到操作数栈
4 // 2.再将整个常量[1]的int类型的值赋值给 局部变量[2]也就是 a = 1;
5 // 同理 b=2也是同样的过程

然后看执行calculate1、calculate2方法

1int c = jvmHello.calculate1(a, b);
2int d = jvmHello.calculate2(a, b);
3// 1.从局部变量[1]中装载引用类型值 即jvmHello的值
4// 2.从局部变量[2]中装载int类型值 即值为2
5// 3.从局部变量[3]中装载int类型值 即值为2
6// 4.使用jvmHello执行calculate1方法
7// 同理 calculate2执行过程类似

上面这段我们知道,jvm在执行代码的时候,是基于的执行,也就是操作栈 每个栈里面有局部变量,局部变量是分配在局部变量表里面

关于java栈的定义,他有两个栈:java虚拟机栈和本地方法栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame )用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务

既然虚拟机栈里面提到线程,那么这里顺便介绍下程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

知道了,继续补充一下我们的图

image-20200116170901654
image-20200116170901654

我们继续分析下calculate1和calculate2

 1private int calculate2(intint);
2    descriptor: (II)I                                                        // 入参2个int类型 出参int类型
3    flags: ACC_PRIVATE
4    Code:
5      stack=3, locals=4, args_size=3                        // 操作数栈3,局部变量4 Slot,参数个数3个
6         0: sipush        666                                        // 将16位带符号整数(这里指666)压入栈
7         3: istore_3                                                        // 将int类型值(即666)存入局部变量[3]
8         4: iload_3                                                            // 从局部变量[3]中装载int类型值
9         5: iload_1                                                            // 从局部变量[1]中装载int类型值
10         6: iload_2                                                            // 从局部变量[3]中装载int类型值
11         7: iadd                                                                // 执行int类型的加法,即 1+2
12         8: idiv                                                                // 执行int类型的除法,即 666/3
13         9: ireturn                                                            // 返回int类型的值
14      LineNumberTable:
15        line 190
16        line 204
17
18  private int calculate1(intint);    
19    descriptor: (II)I                                                        // 入参2个int类型 出参int类型
20    flags: ACC_PRIVATE                                                    // 私有的
21    Code:            
22      stack=2, locals=3, args_size=3                        // 操作数栈2,局部变量3 Slot,参数个数3个
23         0: iload_1                                                            // 从局部变量[1]中装载int类型值 
24         1: iload_2                                                            // 从局部变量[2]中装载int类型值
25         2: iadd                                                                // 执行int类型的加法,即 1+2
26         3: sipush        2333                                    // 将16位带符号整数(这里指2333)压入栈
27         6: imul                                                                // 执行int类型的乘法  3*2333
28         7: ireturn                                                            // 返回int类型的值
29      LineNumberTable:
30        line 240

其实到这里我有个疑问 为什么calculate1和calculate2的入参明明只有2个,反编译后会显示3个呢?我去搜了下

原来在计算args_size时,有判断方法是否为static方法,如果不是static方法,则会在方法原有参数数量上再加一,这是因为非static方法会添加一个默认参数到参数列表首位:方法的真正执行者,即方法所属类的实例对象。那对应我们这多出来的参数就是 jvmHello了

最后关于操作栈的过程 这里我以calculate1为例

image-20200116161136903
image-20200116161136903

上面提到的虚拟机栈的概念也提过,方法执行的同时会创建栈帧,存储局部变量表、操作数栈、动态链接、方法出口;所以上图就是一个栈帧在虚拟机中入栈到出栈的过程.基于这点最后补充一下栈里面的信息内容

image-20200116172500427
image-20200116172500427

116行

表示源文件JvmHello.java

技术总结

通过分析字节码,可以加深对虚拟机内存结构,java代码从编译到加载,和运行的整个过程,而不是去死记书里的那些概念。

参考

END

喜欢的记得一键三连
原文git