前言
《深入理解Java虚拟机》确实是一本神书,但却不太适合入门,太过专业,细节很多,读者很容易就陷入到细节中去,结果看完后,感觉懂了一些,却有感觉串不起来,因此,笔者试着用一张图和13行代码来串下。<br /> 由于能力有限文中有很多地方都很不严谨,甚至描述错误,还请谅解<br /> 阅读建议:看不懂或者有疑问的地方,先不用管,先看完,有个整体把握,然后有问题的再去查资料<br /> <br /> 废话少说,先放出题目中的 一图13行代码 <br />(哈哈,有点标题党了,这么一张图,不过不要被图吓到,下文会慢慢来拆解)
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 类文件到内存中来,将类可执行的代码存放在的方法区中
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.”
该指令调用are的
答案就是:在类加载的解析阶段,因为f()的构造方法只有一个,所有在直接就把are.
(可算是把符合引用和直接引用的概念搞明白了,之前一直不懂,希望理解没错)
静态分派和动态分派
3、 invokevirtual #4 // Method com/hust/concurrency/Lock/Are.f:()I
该指令调用的是are的f()方法,那又是怎么找到f()的地址的呢?
答案就是(个人理解):先根据are引用去堆里找到这个对象,然后,这个对象的对象头里保存了它在方法区中的类结构信息(也就是Are类),然后再在这类结构信息里找到f()的方法信息(通过虚拟方法表),也就是f()方法的第一行指令。<br /> 这里其实就是**静态分派**的过程
那如果再深入一点,现在代码简单,是在编译期就给invokevirtual指定了参数,那如果涉及到继承中的重写等关系时,指令怎么知道调用找到f()的地址呢,这就涉及到**动态分派**的内容了。(《深入 8.3 的第二部分》)<br /> <br /> 看到这里,如果是跟着走了的话,那么对线程栈、指令分析、以及栈桢中操作数栈和局部变量表应该有个简单了解了,那,什么是方法返回地址 和 动态链接呢?
我们接着分析
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问题分析与解决,保准对实战有更多的理解
附件
原图
下载链接: https://pan.baidu.com/s/1cmLLgQiiLYUGHQ_vy8Qm_A 密码: 85th
使用方式:登录 https://www.processon.com/ ,在左上角导入文件就好了
//类加载器示例
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虚拟机》