对基本原理的了解,动手是最好的;html
例子java
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}
复制代码
这段代码我就不解释了 直接编译字节码搞起git
1# 编译生成 JvmHello.class文件
2javac JvmHello.java
3# 反编译字节码内容
4javap -verbose -p JvmHello.class
复制代码
记得以前书里提到的,编译一次处处执行,那么首先文件要被加载进来,运行在一个环境里面;因此咱们有了初步的图github
JvmHello.java -> JvmHello.class -> 类装载系统加载进来 -> 在虚拟机环境执行web
接着咱们看下JVMHello.class的内容shell
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 flags: ACC_PUBLIC, ACC_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 4: return
50 LineNumberTable:
51 line 7: 0
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 0: new #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 28: return
77 LineNumberTable:
78 line 11: 0
79 line 12: 8
80 line 13: 10
81 line 14: 12
82 line 15: 20
83 line 16: 28
84
85 private int calculate2(int, int);
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 19: 0
100 line 20: 4
101
102 private int calculate1(int, int);
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 24: 0
115}
116SourceFile: "JvmHello.java"
复制代码
描述了类的基本信息数组
运行时常量池bash
咱们先分析下第一个常量,位于JVMHello.class第10行,咱们会发现后面有关联项 一块儿放进来oracle
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表示方法定义,右侧的注释内容(表示是由这几行组合起来的)app
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图
方法表集合
先看下静态常量的定义,位于JVMHello.class
1 public static final int i;
2 descriptor: I
3 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
4 ConstantValue: int 2020
复制代码
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 4: return
9 LineNumberTable:
10 line 7: 0
复制代码
这里咱们产生了另外一个概念栈,方法执行会进行压栈出栈
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 0: new #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 28: return // void返回
25 LineNumberTable:
26 line 11: 0
27 line 12: 8
28 line 13: 10
29 line 14: 12
30 line 15: 20
31 line 16: 28
复制代码
从第一行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也是一样的过程
复制代码
而后看执行calculate一、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)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各类虚拟机可能会经过一些更高效的方式去实现),字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。
知道了堆和栈,继续补充一下咱们的图
咱们继续分析下calculate1和calculate2
1private int calculate2(int, int);
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 19: 0
16 line 20: 4
17
18 private int calculate1(int, int);
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 24: 0
复制代码
其实到这里我有个疑问 为何calculate1和calculate2的入参明明只有2个,反编译后会显示2个呢?我去搜了下
原来在计算args_size时,有判断方法是否为static方法,若是不是static方法,则会在方法原有参数数量上再加一,这是由于非static方法会添加一个默认参数到参数列表首位:方法的真正执行者,即方法所属类的实例对象。那对应咱们这多出来的参数就是 jvmHello了
最后关于操做栈的过程 这里我以calculate1为例
上面提到的虚拟机栈的概念也提过,方法执行的同时会建立栈帧,存储局部变量表、操做数栈、动态连接、方法出口;因此上图就是一个栈帧在虚拟机中入栈到出栈的过程.基于这点最后补充一下栈里面的信息内容
表示源文件JvmHello.java
经过分析字节码,能够加深对虚拟机内存结构,java代码从编译到加载,和运行的整个过程,而不是去死记书里的那些概念。
喜欢的记得一键三连
原文git