对基本原理的了解,动手是最好的;
例子
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 复制代码
记得之前书里提到的,编译一次到处执行,那么首先文件要被加载进来,运行在一个环境里面;所以我们有了初步的图
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 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" 复制代码
描述了类的基本信息
运行时常量池
我们先分析下第一个常量,位于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图
方法表集合
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 复制代码
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 4: return 9 LineNumberTable: 10 line 7: 0 复制代码
这里我们产生了另一个概念 栈 ,方法执行会进行压栈出栈
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 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也是同样的过程 复制代码
然后看执行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)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
知道了 堆 和 栈 ,继续补充一下我们的图
我们继续分析下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