java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(1)

前言

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

哪里入手

例子

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; 910    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    }1718    private int calculate2(int a, int b) {19        int x = 666;20        return x / (a   b);21    }2223    private int calculate1(int a, int b) {24        return (a   b) * 2333;25    }26}复制代码

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

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

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

java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(2)

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: 0100        line 20: 4101102  private int calculate1(int, int);103    descriptor: (II)I104    flags: ACC_PRIVATE105    Code:106      stack=2, locals=3, args_size=3107         0: iload_1108         1: iload_2109         2: iadd110         3: sipush        2333111         6: imul112         7: ireturn113      LineNumberTable:114        line 24: 0115}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>":()V2   #6 = Class              #26            // java/lang/Object3  #11 = Utf8               <init>4  #12 = Utf8               ()V5  #22 = NameAndType        #11:#12        // "<init>":()V6  #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/JvmHello2   #3 = Methodref          #2.#22         // com/java/study/jvm/JvmHello."<init>":()V3  #11 = Utf8               <init>4  #12 = Utf8               ()V5  #22 = NameAndType        #11:#12        // "<init>":()V6  #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)I3  #18 = Utf8               (II)I4  #19 = Utf8               calculate15  #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图

java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(3)

36-115行 类内部方法描述

方法表集合

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

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

1  public static final int i;2  descriptor: I3  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL4  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         4: return 9      LineNumberTable:10        line 7: 0复制代码

  • ()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         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: 027        line 12: 828        line 13: 1029        line 14: 1230        line 15: 2031        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类型值 即值为25// 3.从局部变量[3]中装载int类型值 即值为26// 4.使用jvmHello执行calculate1方法7// 同理 calculate2执行过程类似复制代码

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

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

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

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

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

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

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

java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(4)


我们继续分析下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 212         8: idiv                                                                // 执行int类型的除法,即 666/313         9: ireturn                                                            // 返回int类型的值14      LineNumberTable:15        line 19: 016        line 20: 41718  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 226         3: sipush        2333                                    // 将16位带符号整数(这里指2333)压入栈27         6: imul                                                                // 执行int类型的乘法  3*233328         7: ireturn                                                            // 返回int类型的值29      LineNumberTable:30        line 24: 0复制代码

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

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

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

java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(5)

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

java核心知识整理jvm篇(那些不需死记硬背的jvm基本原理)(6)

116行

表示源文件JvmHello.java

技术总结

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


作者:张大佛爷_zhang

链接:https://juejin.im/post/5e2030f2e51d45026d6ee155

,