前言

  1. 《深入理解Java虚拟机》确实是一本神书,但却不太适合入门,太过专业,细节很多,读者很容易就陷入到细节中去,结果看完后,感觉懂了一些,却有感觉串不起来,因此,笔者试着用一张图和13行代码来串下。<br /> 由于能力有限文中有很多地方都很不严谨,甚至描述错误,还请谅解<br /> 阅读建议:看不懂或者有疑问的地方,先不用管,先看完,有个整体把握,然后有问题的再去查资料<br /> <br /> 废话少说,先放出题目中的 一图13行代码 <br />(哈哈,有点标题党了,这么一张图,不过不要被图吓到,下文会慢慢来拆解)

啊啊啊.png

  1. public class Demo {
  2. public static void main(String[] args) {
  3. //这里搞个对象,就是为了引出对象创建过程
  4. Are are = new Are();
  5. are.f();
  6. }
  7. }
  8. public class Are {
  9. public int f(){
  10. int a=33,b=44;
  11. return 33+44;
  12. }
  13. }

零、JVM概览

先来张图JVM的整体结构图,来打个印象!
image.png
(下面这段话,看不太懂没关系,等全文看完,再回过头来看试试)

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文件)
(看到这里,可以跳到文章的第二章了)
image.png

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


二、类加载

写在前面:类加载流程和细节已经在《深入 7.3》已经讲的很好了,在这里仅仅是以图的形式更直观的表达一些内容
JVM 的类加载器从磁盘上加载Demo.class 类文件到内存中来,将类可执行的代码存放在的方法区中

image.png
图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
指令 先不管 指令参数
image.png

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

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()的地址的呢?

  1. 答案就是(个人理解):先根据are引用去堆里找到这个对象,然后,这个对象的对象头里保存了它在方法区中的类结构信息(也就是Are类),然后再在这类结构信息里找到f()的方法信息(通过虚拟方法表),也就是f()方法的第一行指令。<br /> 这里其实就是**静态分派**的过程
  2. 那如果再深入一点,现在代码简单,是在编译期就给invokevirtual指定了参数,那如果涉及到继承中的重写等关系时,指令怎么知道调用找到f()的地址呢,这就涉及到**动态分派**的内容了。(《深入 8.3 的第二部分》)<br /> <br /> 看到这里,如果是跟着走了的话,那么对线程栈、指令分析、以及栈桢中操作数栈和局部变量表应该有个简单了解了,那,什么是方法返回地址 动态链接呢?

我们接着分析


3.1 f()执行过程

  1. public class Demo {
  2. public static void main(String[] args) {
  3. //这里搞个对象,就是为了引出对象创建过程
  4. Are are = new Are();
  5. are.f();
  6. }
  7. }
  8. public class Are {
  9. public int f(){
  10. int a=33,b=44;
  11. return 33+44;
  12. }
  13. }

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

关于中间的计算过程以及流程转变,强烈推荐下《深入 8.4.3》的例子,讲这个过程讲的很透
image.png
这里仅说我的几点理解

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问题分析与解决,保准对实战有更多的理解


附件

原图

下载链接: https://pan.baidu.com/s/1cmLLgQiiLYUGHQ_vy8Qm_A 密码: 85th

使用方式:登录 https://www.processon.com/ ,在左上角导入文件就好了
image.png

  1. //类加载器示例
  2. public class Loader {
  3. public static void main(String[] args) {
  4. System.out.println(String.class.getClassLoader());
  5. System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
  6. System.out.println(Demo.class.getClassLoader().getClass().getName());
  7. }
  8. }
  9. 输出
  10. //启动类加载器
  11. null
  12. //扩展加载器
  13. sun.misc.Launcher$ExtClassLoader
  14. //应用程序加载器
  15. sun.misc.Launcher$AppClassLoader

Demo.class

  1. Classfile /Users/admin/Documents/longfor/Concurrency/target/classes/com/hust/concurrency/Lock/Demo.class
  2. Last modified 2022-1-22; size 538 bytes
  3. MD5 checksum 17b6e030f829d88a29120792bc90cdf4
  4. Compiled from "Demo.java"
  5. public class com.hust.concurrency.Lock.Demo
  6. minor version: 0
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER
  9. Constant pool:
  10. #1 = Methodref #6.#22 // java/lang/Object."<init>":()V
  11. #2 = Class #23 // com/hust/concurrency/Lock/Are
  12. #3 = Methodref #2.#22 // com/hust/concurrency/Lock/Are."<init>":()V
  13. #4 = Methodref #2.#24 // com/hust/concurrency/Lock/Are.f:()I
  14. #5 = Class #25 // com/hust/concurrency/Lock/Demo
  15. #6 = Class #26 // java/lang/Object
  16. #7 = Utf8 <init>
  17. #8 = Utf8 ()V
  18. #9 = Utf8 Code
  19. #10 = Utf8 LineNumberTable
  20. #11 = Utf8 LocalVariableTable
  21. #12 = Utf8 this
  22. #13 = Utf8 Lcom/hust/concurrency/Lock/Demo;
  23. #14 = Utf8 main
  24. #15 = Utf8 ([Ljava/lang/String;)V
  25. #16 = Utf8 args
  26. #17 = Utf8 [Ljava/lang/String;
  27. #18 = Utf8 are
  28. #19 = Utf8 Lcom/hust/concurrency/Lock/Are;
  29. #20 = Utf8 SourceFile
  30. #21 = Utf8 Demo.java
  31. #22 = NameAndType #7:#8 // "<init>":()V
  32. #23 = Utf8 com/hust/concurrency/Lock/Are
  33. #24 = NameAndType #27:#28 // f:()I
  34. #25 = Utf8 com/hust/concurrency/Lock/Demo
  35. #26 = Utf8 java/lang/Object
  36. #27 = Utf8 f
  37. #28 = Utf8 ()I
  38. {
  39. public com.hust.concurrency.Lock.Demo();
  40. descriptor: ()V
  41. flags: ACC_PUBLIC
  42. Code:
  43. stack=1, locals=1, args_size=1
  44. 0: aload_0
  45. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  46. 4: return
  47. LineNumberTable:
  48. line 4: 0
  49. LocalVariableTable:
  50. Start Length Slot Name Signature
  51. 0 5 0 this Lcom/hust/concurrency/Lock/Demo;
  52. //该main()方法的描述符号
  53. public static void main(java.lang.String[]);
  54. descriptor: ([Ljava/lang/String;)V
  55. //权限
  56. flags: ACC_PUBLIC, ACC_STATIC
  57. Code:
  58. //stack=2 操作数栈的最大深度为2 ,locals=2 局部变量表的最大容量
  59. stack=2, locals=2, args_size=1
  60. //创建Are对象,并将其引用值压入栈顶 (#2 表示调用的是Constant pool中的#2 对应的内容 //后面的内容其实也就是注释)
  61. 0: new #2 // class com/hust/concurrency/Lock/Are
  62. //复制栈顶元素(对象are的引用)
  63. 3: dup
  64. //调用Are的<init()>方法 即构造器方法
  65. 4: invokespecial #3 // Method com/hust/concurrency/Lock/Are."<init>":()V
  66. //将操作数栈顶存到局部变量表的索引1位置 就是程序员理解的赋值过程
  67. 7: astore_1
  68. //将索引1位置的值 压入到操作数栈顶(即are对象的引用)
  69. 8: aload_1
  70. //调用实例方法are.f(),参数操作数栈里的内容(这里就是对象的引用);
  71. //该方法执行完成后,会将结果放到栈顶
  72. //问题:f()方法是在自己的栈桢里执行的,那它是怎么将自己的操作数栈顶的内容压入到main()栈桢中的呢?动态链接!
  73. 9: invokevirtual #4 // Method com/hust/concurrency/Lock/Are.f:()I
  74. //将操作数栈顶数值弹出
  75. 12: pop
  76. //返回
  77. 13: return
  78. //这个是啥,还真不清楚
  79. LineNumberTable:
  80. line 6: 0
  81. line 7: 8
  82. line 8: 13
  83. //局部变量表的内容
  84. LocalVariableTable:
  85. Start Length Slot Name Signature
  86. 0 14 0 args [Ljava/lang/String;
  87. 8 6 1 are Lcom/hust/concurrency/Lock/Are;

Area.class

  1. Classfile /Users/admin/Documents/longfor/Concurrency/target/classes/com/hust/concurrency/Lock/Are.class
  2. Last modified 2022-1-22; size 397 bytes
  3. MD5 checksum b56fab2833d5b67c80c231ed7e699559
  4. Compiled from "Are.java"
  5. public class com.hust.concurrency.Lock.Are
  6. minor version: 0
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER
  9. Constant pool:
  10. #1 = Methodref #3.#18 // java/lang/Object."<init>":()V
  11. #2 = Class #19 // com/hust/concurrency/Lock/Are
  12. #3 = Class #20 // java/lang/Object
  13. #4 = Utf8 <init>
  14. #5 = Utf8 ()V
  15. #6 = Utf8 Code
  16. #7 = Utf8 LineNumberTable
  17. #8 = Utf8 LocalVariableTable
  18. #9 = Utf8 this
  19. #10 = Utf8 Lcom/hust/concurrency/Lock/Are;
  20. #11 = Utf8 f
  21. #12 = Utf8 ()I
  22. #13 = Utf8 a
  23. #14 = Utf8 I
  24. #15 = Utf8 b
  25. #16 = Utf8 SourceFile
  26. #17 = Utf8 Are.java
  27. #18 = NameAndType #4:#5 // "<init>":()V
  28. #19 = Utf8 com/hust/concurrency/Lock/Are
  29. #20 = Utf8 java/lang/Object
  30. {
  31. public com.hust.concurrency.Lock.Are();
  32. descriptor: ()V
  33. flags: ACC_PUBLIC
  34. Code:
  35. stack=1, locals=1, args_size=1
  36. 0: aload_0
  37. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  38. 4: return
  39. LineNumberTable:
  40. line 8: 0
  41. LocalVariableTable:
  42. Start Length Slot Name Signature
  43. 0 5 0 this Lcom/hust/concurrency/Lock/Are;
  44. public int f();
  45. descriptor: ()I
  46. flags: ACC_PUBLIC
  47. Code:
  48. stack=1, locals=3, args_size=1
  49. 0: bipush 33
  50. 2: istore_1
  51. 3: bipush 44
  52. 5: istore_2
  53. 6: bipush 77
  54. 8: ireturn
  55. LineNumberTable:
  56. line 11: 0
  57. line 12: 6
  58. LocalVariableTable:
  59. Start Length Slot Name Signature
  60. 0 9 0 this Lcom/hust/concurrency/Lock/Are;
  61. 3 6 1 a I
  62. 6 3 2 b I
  63. }
  64. SourceFile: "Are.java"
  1. //类加载器示例
  2. public class Loader {
  3. public static void main(String[] args) {
  4. System.out.println(String.class.getClassLoader());
  5. System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
  6. System.out.println(Loader.class.getClassLoader().getClass().getName());
  7. }
  8. }
  9. 输出
  10. //启动类加载器
  11. null
  12. //扩展加载器
  13. sun.misc.Launcher$ExtClassLoader
  14. //应用程序加载器
  15. sun.misc.Launcher$AppClassLoader

参考

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