一图13行代码,带你串通JVM

327 阅读10分钟

​ 本文已参与「新人创作礼」活动,一起开启掘金创作之路

 前言

《深入理解Java虚拟机》确实是一本神书,但却不太适合入门,太过专业,细节很多,读者很容易就陷入到细节中去,结果看完后,感觉懂了一些,却有感觉串不起来,因此,笔者试着用一张图和13行代码来串下。

由于能力有限文中有很多地方都很不严谨,甚至描述错误,还请谅解

阅读建议:看不懂或者有疑问的地方,先不用管,先看完,有个整体把握,然后有问题的再去查资料

废话少说,先放出题目中的 一图13行代码

(哈哈,有点标题党了,这么一张图,不过不要被图吓到,下文会慢慢来拆解)

​编辑

public class Demo {
    public static void main(String[] args) {
        //这里搞个对象,就是为了引出对象创建过程
        Are are = new Are();
        are.f();
    }
}
public class Are {
    public int f(){
        int a=33,b=44;
        return 33+44;
    }
}

零、JVM概览

先来张图JVM的整体结构图,来打个印象!

​编辑

(下面这段话,看不太懂没关系,等全文看完,再回过头来看试试)

0、首先,是上面的代码,经过编译后,生成两个Demo.class,Are.class文件后,运行 java Demo.java 命令后,程序正式开始运行!

(这里就涉及到编译和class文件了,对应《深入理解Java虚拟机 第2版》的第6章和第四大部分, 后文统称《深入》)

1、首先, JVM 的类加载器从磁盘上加载Demo.class 类文件到内存中来,将类可执行的代码存放在的方法区中。

(你看,这里就是涉及到什么是class文件、类加载过程以及运行时数据区,对应《深入 2.2.5、6、7.3 》)

2、加载完成后,JVM 创建一个主线程执行这个类文件的 main 方法,main 方法的输入参数和方法内定义的变量被压入主线程对应的一个栈。

(这里说线程对应的栈,就是Java虚拟机栈;而压入的过程其实也是执行引擎执行指令的过程,对应《深入》的第2.2、8.3)

3、如果在方法内创建了一个对象实例,这个对象实例信息将会被存放到堆里,而对象实例的引用,也就是对象实例在堆中的地址信息则会被记录在栈里。

(这里就涉及到对象的创建时机(《深入7.2》)、创建过程、以及对象的引用访问过程 (深入 2.3》)

4、程序计数器一开始存放的是 main 方法的第一行代码位置,JVM 的执行引擎根据这个位置去方法区的对应位置加载这行代码指令,将其解释为自身所在平台的 CPU 指令后交给 CPU 执行。

(到这里,程序计数器就出来,同时,"解释"的概念也出来了。对应《深入 2.2.1》《深入 8.4》)

5、如果在 main 方法里调用了其他方法,那么在进入其他方法的时候,会在 Java 栈中为这个方法创建一个新的栈帧,当线程在这个方法内执行的时候,方法内的局部变量都存放在这个栈帧里。当这个方法执行完毕退出的时候,就把这个栈帧从 Java 栈中出栈,这样当前栈帧,也就是堆栈的栈顶就又回到了 main 方法的栈帧,使用这个栈帧里的变量,继续执行 main 方法。

(方法调用、当前栈桢的概念也就出来了,对应《深入 8.2》《深入 8.3》)

6、在整个程序执行过程中,JVM创建一些后台线程进行内存的自动分配和回收

(在这里,就出现最重要的垃圾回收了,对应《深入 第3章》)


下面,开始拆解图啦!

一、编译

关于编译,简单理解就是把 开头的.java文件格式的代码,编译成类文件(.class文件)

(看到这里,可以跳到文章的第二章了)

​编辑

这部分涉及到当然,像一些Java语法糖的东西,还是需要了解下,这部分的内容在《深入》的第10章和第11章


二、类加载

写在前面:类加载流程和细节已经在《深入 7.3》已经讲的很好了,在这里仅仅是以图的形式更直观的表达一些内容

JVM 的类加载器从磁盘上加载Demo.class 类文件到内存中来,将类可执行的代码存放在的方法区中

​编辑

图3 类加载过程

说下我的一些补充

2.1 类加载器

1、首先C++创建一个启动类加载器实例(Bootstrap ClassLoader),来加载jdk核心类如String),而后,JVM创建一个扩展类加载器,也是加载一些重要类,紧接创建一个应用程序类加载器,来加载Demo类

2、其次,在初始化阶段,执行的( ()是不需要定义,是编译器自动收集所有类变量和静态代码块的语句,合并而成的方法)(在对象创建创建过程中的,()方法对应的就是构造器方法)

(下面的纯粹是为了文章目录完整,可以直接跳到第三部分)

2.2 双亲委派模型

在上面的类加载过程,我们说到很多加载器,那这个时候,就涉及到加载器之间是怎么协同工作的了,其中一种策略就是双亲委派,具体的细节也不讲了,见《深入 7.4》


三 字节码执行引擎

其实,从类加载到整个内存回收,都涉及到执行引擎,在这里这简单介绍执行引擎在方法执行上的一些知识点。

3.1 main()执行过程

1、首先,在主线程启动时,会创建一个线程栈用于代码(为什么是栈,这个自己去找答案)

2、接着,给main()方法创建一个栈桢,并压入到线程栈中,此时main栈桢就是当前栈

3、程序计数器指向main()的第一个指令,然后,开始一条条指令执行

在看下图之前,首先得有个指令的概念;

指令分析

(更多的分析,网上一堆)

new #2 // class com/hust/concurrency/Lock/Are

指令 先不管 指令参数

​编辑

现在来看这里面涉及到哪些东西哈?

1、第一条指令就是new指令,也就是创建对象,那我们就可以去了解对象创建过程了(《深入 2.3.1 》)

2、invokespecial #3 // Method com/hust/concurrency/Lock/Are."":()V

该指令调用are的方法,也就是构造器方法;那是怎么找到的呢?

答案就是:在类加载的解析阶段,因为f()的构造方法只有一个,所有在直接就把are.的符号引用(也就是 com/hust/concurrency/Lock/Are."":()V)替换成了are.内存地址的地址。(《深入 8.3.1 》)

(可算是把符合引用和直接引用的概念搞明白了,之前一直不懂,希望理解没错)

静态分派和动态分派

3、 invokevirtual #4 // Method com/hust/concurrency/Lock/Are.f:()I

该指令调用的是are的f()方法,那又是怎么找到f()的地址的呢?

答案就是:先根据are引用去堆里找到这个对象,然后,这个对象的对象头里保存了它在方法区中的类结构信息(也就是Are类),然后再在这类结构信息里找到f()的方法信息,也就是f()方法的第一行指令。

这里其实就是静态分派的过程

那如果再深入一点,现在代码简单,是在编译期就给invokevirtual指定了参数,那如果涉及到继承中的重写等关系时,指令怎么知道调用找到f()的地址呢,这就涉及到动态分派的内容了。(《深入 8.3 的第二部分》)

看到这里,如果是跟着走了的话,那么对线程栈、指令分析、以及栈桢中操作数栈和局部变量表应该有个简单了解了,那,什么是方法返回地址 和 动态链接呢?

我们接着分析


3.1 f()执行过程

public class Demo {
    public static void main(String[] args) {
        //这里搞个对象,就是为了引出对象创建过程
        Are are = new Are();
        are.f();
    }
}
public class Are {
    public int f(){
        int a=33,b=44;
        return 33+44;
    }
}

好,我解释下下这张图的前置条件,从代码可以看出,f()是由在main()方法中,有are对象调用的,那么,在调用之前,已经有了are对象和线程栈以及main栈桢。

关于中间的计算过程以及流程转变,强烈推荐下《深入 8.4.3》的例子,讲这个过程讲的很透

​编辑

这里仅说我的几点理解

1、方法返回地址干嘛的?

简单来说,在main()方法调用f()时(也就是3.2中的invokevirtual指令),会将调用时当前指令所在的地址保存到f()栈桢上的方法返回地址中。而f()方法的最后一条指令

ireturn指令需要将结果压入到调用者(main()栈桢)的操作数栈顶,依赖的就是方法出口保存的地址。

2、局部变量表

简单来说就是以数组的形式存储方法中的临时变量,其中,第一个索引比较特别,就是调用这个方法的对象的引用,也就是我们这里的are;

(到这里,还是不知道动态链接是干嘛的...算了,继续)

编译优化+解释执行

其实,上面有太多的细节可以挖,由于篇幅有限,这里仅提两句

1、为什么实际的指令为什么和理想指令不一样,在编译的时候,JVM是怎么优化的(《深入》的第10章和地11章就对这些进行内容进行了介绍);

2、我们这里说的指令什么的,我们能看懂,而CPU压根就不清楚的阿,这就涉及到解释执行和汇编方面的知识(《深入6.4.1》简单提及了一些)


四、对象创建和内存分配回收

4.1 对象创建

在main()方法中,第一条指令就是创建对象了,关于这部分,也其实没啥好说的,看《深入 2.3.1 和 7.2》就完事了

0: new #2 // class com/hust/concurrency/Lock/Are

4.2 内存分配回收

同样,也没好说的,写出来纯粹是为了篇幅完整,不过建议在阅读《深入 3》的基础上,

看看这篇 Java中9种常见的CMS GC问题分析与解决,保准对实战有更多的理解


附件

原图

下载链接: 百度网盘 请输入提取码 密码: 85th

使用方式:登录 ProcessOn - 免费在线作图,思维导图,流程图,实时协作 ,在左上角导入文件就好了

​编辑

//类加载器示例
public class Loader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(Demo.class.getClassLoader().getClass().getName());
    }
}
输出
//启动类加载器
null 
//扩展加载器
sun.misc.Launcher$ExtClassLoader
//应用程序加载器
sun.misc.Launcher$AppClassLoader

Demo.class

Classfile /Users/admin/Documents/longfor/Concurrency/target/classes/com/hust/concurrency/Lock/Demo.class
  Last modified 2022-1-22; size 538 bytes
  MD5 checksum 17b6e030f829d88a29120792bc90cdf4
  Compiled from "Demo.java"
public class com.hust.concurrency.Lock.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/hust/concurrency/Lock/Are
   #3 = Methodref          #2.#22         // com/hust/concurrency/Lock/Are."<init>":()V
   #4 = Methodref          #2.#24         // com/hust/concurrency/Lock/Are.f:()I
   #5 = Class              #25            // com/hust/concurrency/Lock/Demo
   #6 = Class              #26            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/hust/concurrency/Lock/Demo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               are
  #19 = Utf8               Lcom/hust/concurrency/Lock/Are;
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = Utf8               com/hust/concurrency/Lock/Are
  #24 = NameAndType        #27:#28        // f:()I
  #25 = Utf8               com/hust/concurrency/Lock/Demo
  #26 = Utf8               java/lang/Object
  #27 = Utf8               f
  #28 = Utf8               ()I
{
  public com.hust.concurrency.Lock.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hust/concurrency/Lock/Demo;

//该main()方法的描述符号
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    //权限
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      //stack=2 操作数栈的最大深度为2 ,locals=2 局部变量表的最大容量 
      stack=2, locals=2, args_size=1
         //创建Are对象,并将其引用值压入栈顶 (#2 表示调用的是Constant pool中的#2 对应的内容 //后面的内容其实也就是注释)
         0: new           #2                  // class com/hust/concurrency/Lock/Are
         //复制栈顶元素(对象are的引用)
         3: dup
         //调用Are的<init()>方法 即构造器方法
         4: invokespecial #3                  // Method com/hust/concurrency/Lock/Are."<init>":()V
         //将操作数栈顶存到局部变量表的索引1位置 就是程序员理解的赋值过程 
         7: astore_1
         //将索引1位置的值 压入到操作数栈顶(即are对象的引用)
         8: aload_1
         //调用实例方法are.f(),参数操作数栈里的内容(这里就是对象的引用);
         //该方法执行完成后,会将结果放到栈顶
         //问题:f()方法是在自己的栈桢里执行的,那它是怎么将自己的操作数栈顶的内容压入到main()栈桢中的呢?动态链接!
         9: invokevirtual #4                  // Method com/hust/concurrency/Lock/Are.f:()I
        //将操作数栈顶数值弹出
        12: pop
        //返回
        13: return
      //这个是啥,还真不清楚
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 13
      //局部变量表的内容
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1   are   Lcom/hust/concurrency/Lock/Are;

Area.class

Classfile /Users/admin/Documents/longfor/Concurrency/target/classes/com/hust/concurrency/Lock/Are.class
  Last modified 2022-1-22; size 397 bytes
  MD5 checksum b56fab2833d5b67c80c231ed7e699559
  Compiled from "Are.java"
public class com.hust.concurrency.Lock.Are
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // com/hust/concurrency/Lock/Are
   #3 = Class              #20            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/hust/concurrency/Lock/Are;
  #11 = Utf8               f
  #12 = Utf8               ()I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               b
  #16 = Utf8               SourceFile
  #17 = Utf8               Are.java
  #18 = NameAndType        #4:#5          // "<init>":()V
  #19 = Utf8               com/hust/concurrency/Lock/Are
  #20 = Utf8               java/lang/Object
{
  public com.hust.concurrency.Lock.Are();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hust/concurrency/Lock/Are;

public int f();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: bipush        33
         2: istore_1
         3: bipush        44
         5: istore_2
         6: bipush        77
         8: ireturn
      LineNumberTable:
        line 11: 0
        line 12: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/hust/concurrency/Lock/Are;
            3       6     1     a   I
            6       3     2     b   I
 }
SourceFile: "Are.java"
//类加载器示例
public class Loader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(Loader.class.getClassLoader().getClass().getName());
    }
}
输出
//启动类加载器
null 
//扩展加载器
sun.misc.Launcher$ExtClassLoader
//应用程序加载器
sun.misc.Launcher$AppClassLoader

参考

1、《深入理解Java虚拟机》