JVM 是什么?
Java virtual Machine
JVM 全称 Java Virtual Machine ,也就是我们经常说的Java虚拟机,是一种计算机设备的规范,是一个虚拟的计算机。它能识别 .class 后缀的文件,能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
JVM的作用?
在Java程序中,使用 Javac 命令编译 .Java 文件来生成 .class 文件,还需要使用Java命令去主动执行它,但是操作系统并不认识这些 .class 文件,所以JVM此时就相当于翻译字节码文件,由JVM将程序解析给本地系统执行。

在图中,有JVM 层,Java程序就可以实现跨平台了。JVM只需保证正确执行 .class 文件,就可以在Linux、 Windows、macOS等系统上运行。
JVM的特性
网上总结的JVM特性一般都是有三点:移植性、成熟、覆盖面。
但是归根结底总结也就两大特性:
-
*跨平台性
当我们写一段HelloWord代码的时候,不管在哪个平台(Linux、Windows、macOS)等平台执行,最终实现的效果都是一样的,这个就是JVM的跨平台特性。为了实现跨平台性,不同操作系统有不同的JDK版本
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html

-
跨语言
JVM只识别字节码,所以JVM其实是跟语言解耦的,没有直接关联,这里有个注意点JVM不是翻译Java文件,而是识别.class 文件,一般称之为字节码。除了Java以外,常见的Kotlin、Groovy、Jruby等语言,它们也是编译成字节码,也是可以运行在JVM上,这就是JVM的跨语言特性。
JVM、JRE 和 JDK的关系?
JVM只是一个翻译,把字节码翻译成机器所能识别的代码,注意:JVM不会自己生成代码,需要我们编写代码,同时需要依赖很多类库,而这时候就需要用到JRE了。
JRE 全称是:Java Runtime Environment 即Java运行环境,它除了包含JVM之外,还提供了很多类库(如:文件操作、网络连接、等等一些类库),这些都是JRE提供的基础类库。JVM标准上加上实现的一大堆基础类库,就组成了Java运行时环境。注意:JRE是运行环境,并不是一个开发环境,没有包含任何开发工具,只是针对使用Java程序的用户。
JDK 是Java开发工具包,主要是面向程序员,JDK的安装目录下有个jre目录,里面有两个文件夹bin和lib,可以认为bin就是JVM,而lib中则是JVM运行所需要的类库。JDK是整个Java的核心,包含了Java运行时环境JRE、一些Java工具(javac、java、javap和jdb等)和Java基础的类库(Java API)。
总结:JDK包含JRE,JRE又包含JVM。
JVM整体
一个Java程序,首先经过 javac 编译成为 .class 文件,然后由JVM加载到方法区,执行引擎将会执行这些字节码。执行时,会将字节码翻译成为操作系统相关函数。JVM 作为 .class 的翻译存在,输入字节码,调用操作系统函数。
过程:Java 文件 —> 编译器 —> 字节码 —> JVM —> 机器码
我们所说的JVM,狭义上指的是HotSpot 虚拟机(JVM版本很多,但是使用最多的是HotSpot 虚拟机,还有JRockit和IBM J9虚拟机)。
JVM内存划分
Java引以为豪的是它的自动内存管理机制,相比于c、c++的手动内存管理、复杂难以理解的指针等,Java程序写起来就方便多了。
Java的JVM内存布局分为以下几部分:

程序计算器(Program Counter Register)
Java程序是多线程的,CPU可以在多个线程中随机分配执行时间片段,当执行的线程超过CPU核心数时,线程之间会根据时间片轮训争夺CPU资源。当某个线程被CPU挂起时,需要记录当前代码已经执行的位置,方便CPU重新执行执行此线程时,知道从哪行指令开始执行,这就是程序计数器的作用。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码地址,例如分支操作、循环操作、跳转、异常处理、线程恢复等都依赖计数器

关于程序计算器有以下几点需要格外注意:
- 在 Java 虚拟机规范中,对程序计数器这块区域没有规定任何 OutOfMemoryError 错误
- 它是线程私有的,每条线程内部都有一个私有程序计算器,它的生命周期随着线程的创建而创建,线程结束它跟着死亡。
- 当一个线程正在执行一个 Java 方法的时候,这个程序计数器记录的是正在执行虚拟机字节码指令的地址。如果执行的是 native 方法,这个计算器值则为空(Undefined)。
虚拟机栈
虚拟机栈也是线程私有的,与线程的生命周期同步。在 JVM 规范中,对这个区域固定了两种异常状况:
- StackOverflowError:当线程请求栈深度超出虚拟机栈所运行的深度时抛出。
- OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
学习 Java 虚拟机的过程中,经常看到一句话:
JVM 是基于栈的解析器执行的,DVM 是基于寄存器解析器执行的。
上面这句话里的 “基于栈” 指的是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM虚拟机都会在虚拟机栈中创建一个 栈帧。
那么 栈 是什么样的数据结构?
栈是一种先进后出(FILO)的数据结构,一个线程(虚拟机栈)包含多个栈帧,大家可以想象成为子弹夹,压入子弹叫入栈,子弹打出叫出栈,先被压进的最后被打出。
栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k

栈帧
栈帧(Stack Frame) 是用于支持虚拟机执行方法调用和方法执行的数据结构,每一个线程执行一个方法,都会为这个方法创建一个栈帧(如常见的 main 方法,虚拟机会为之创建一个 main 栈帧),并入栈,一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
每个栈帧包含四个部分:局部变量表、操作数栈、动态链接、返回地址。
局部变量表:
顾名思义就是局表变量的表,用来存放局部变量的。首先它是一个32位的长度,主要存放 Java 八大基本数据类型,如果局部是Object对象,则存放是它的引用地址。
操作数栈:
存放方法执行的操作数的。它也是一个栈,先进后出的栈结构,操作数栈用来操作的,操作的元素可以是任意的 Java 数据类型,所以我们知道一个方法刚开始的时候,这个方法的操作数栈是空,在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。
动态连接:
动态连接主要目的是为了支持方法调用过程中的动态连接。
在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。
Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量迟中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接。
返回地址:
当一个方法开始执行后,只有两种方式可以退出这个方法
- 正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如:return)并退出,没有抛任何异常
- 异常退出:指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
实战讲解
用一个简单的add()方法来显示,代码如下:
public class DemoTest {
public int add() {
int a = 2;
int b = 3;
int c = (a + b) * 6;
return c;
}
}
编译成 .class 文件后,我们使用 javap -v DemoTest.class 命令来查看这个类的字节码,add() 方法字节码结果如下:
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 6
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 12: 0
line 13: 2
line 14: 4
line 15: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/haegyeong/pipi/DemoTest;
2 11 1 a I
4 9 2 b I
11 2 3 c I
敲黑板:
locals:表示该栈帧的局部变量表的长度为4
LocalVariableTable:用于存放运行期间和操作数栈交互(出栈/入栈)的局部变量,如果是非静态的方法,局部变量表存放的第一个都是 this 对象本身,毕竟方法中可能会用到成员变量,得有个引用,静态方法则没有 this 。
0: iconst_2 (将常量 2 压入操作数栈栈顶)
1: istore_1 (将操作数栈栈顶的出栈,存放在局部变量表中索引下标为 1 的位置)
2: iconst_3 (将常量 3 压入操作数栈栈顶)
3: istore_2 (将操作数栈栈顶的出栈,存放在局部变量表中索引下标为 2 的位置)
4: iload_1 (取出在局部变量表中下标索引为 1 的值,压入到操作数栈的栈顶)
5: iload_2 (取出在局部变量表中下标索引为 2 的值,压入到操作数栈的栈顶)
6: iadd (将操作数栈栈顶两个数字弹出,进行求和,然后再将结果压入栈顶)
7: bipush 6 (将常量值 6 压入操作数栈的栈顶)
9: imul (将操作数栈栈顶两个数字弹出,进行乘积,然后再将结果压入栈顶)
10: istore_3 (将操作数栈栈顶的出栈,存放在局部变量表中索引下标为 3 的位置)
11: iload_3 (将局部变量表中索引下标为 3 的取出,压入操作数栈的栈顶)
12: ireturn (将操作数栈栈顶结果返回)
注意,字节码前面都有一个数字,每要执行一行,程序计算器就会先记录当前执行的行号,细心的同学可能发现 7 怎么就跳到了9 了,8怎么不见,这里面就涉及到一个偏移量的问题,这块我也不太懂,大家谷歌一下吧。
看到上面字节码的一个执行过程,对 JVM 是基于栈的解析器执行的 这句话有没有更加深入的了解呢?
没错,就是在操作数栈进行一个出栈、入栈的操作而已。
iconst 和 bipush: 这两个指令都是将常量压入操作数栈,如果取值范围是 -1~5 则采用 iconst 入栈指令,如果取值是-128~127 之间(除了-1~5的范围),则使用 bipush 入栈指令。还有 sipush、LDC 等指令,具体看数字。
istore:将操作数栈栈顶元素存放如局部变量表的某索引位置。 比如上面: istore_1 则代表将操作数栈栈顶元素存放在局部变量表中下标为 1 的位置。
iload:将局部变量表中某索引位置的值取出并压入到操作数栈栈顶位置。如上面:iload_1 将局部变量表中索引为 1 的值取出,并压缩操作数栈的栈顶位置。
iadd: 将操作数栈栈顶的两个元素弹出进行相加,并将结果重新压入操作数栈的栈顶。
imul:将操作数栈栈顶的两个元素弹出进行,并将结果重新压入操作数栈的栈顶。
字节码助记码解释地址:https://cloud.tencent.com/developer/article/1333540
画图简单显示一下add方法的过程,代码执行到add 方法时,局部变量表和操作数栈的情况如下:

iconst_2 把常量值 2 压入操作数栈,效果图如下:

istore_1 把操作数栈栈顶的值弹出,把存放到局部变量表中索引为 1 的位置 ,效果图如下:

此时,局部变量表中下标为1 已经有一个2 数值了,而操作数栈此时为空。
iconst_3 将常量值 3 压入操作操作数栈栈顶,效果图如下:

istore_2 把操作数栈栈顶的值弹出,把存放到局部变量表中索引为2 的位置 ,效果图如下:

iload_1 和 iload_2 这两步是将局部变量表中下标为 1 和 2 的值压入操作数栈,结果如下:

iadd 将操作数栈栈顶两个数字取出,并计算相加的结果,将结果重新压入操作数栈,结果如下:

bipush 这个也是个入栈操作,将6压入操作数栈栈顶,效果如下:

imul 将操作数栈栈顶两个数字5 和 6 弹出,进行乘积,然后再将结果压入栈顶,结果如下:

istore_3 将操作数栈栈顶的30 出栈,存放在局部变量表中索引下标为 3 的位置,效果如下:

iload_3 将局部变量表中索引下标为 3 的值 30 取出,压入操作数栈的栈顶,效果图如下:

ireturn 将操作数栈顶的元素 30 返回给上层方法。至此 add() 方法执行完毕。局部变量表和操作数栈也会相继被销毁。
注意:局部表量表中下标为 0 的 this 是栈帧创建就会默认存放的,静态方法没有这个。
本地方法栈
本地方法栈和上面的 Java 虚拟机栈类似,只不过 Java 虚拟机栈是管理 Java 函数的调用,而本地方法栈是用于管理本地方法的调用。本地方法不是用 Java 实现的,而是由 c 语言实现。本地方法栈是针对 native 方法,Android 开发中如果涉及到系统底层或者 JNI 可能就接触本地方法栈多一些。不同虚拟机有不同实现,在Hotspot 虚拟机的实现,已经将虚拟机栈和本地方法栈中合二为一了(后面带你眼见为实)。
方法区 -线程共享
方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。主要用来存放已经被 JVM 虚拟机加载过的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池、即时编译器编译后的代码和数据。
Hotspot 虚拟机在 jdk1.7 之前使用永久区来实现方法区,在 jdk 1.8 之后,永久区已经被移除了,取而代之的是一个叫做 元空间(metaspace)的实现方式。在其他虚拟机,如:Oracle 的JRockit、IBM 的 J9 是不存在永久代一说。
JVM 在执行某个类的时候,必须要先加载(类加载的五大步骤:加载、验证、准备、解析、初始化),JVM 会先加载 class 文件,而在 class文件中出了又类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串、基本类型的常量(final 修饰的变量),符号应用则包括类和方法的全限定名(如:String 全限定名是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。
当类加载到内存中后,JVM 就会将 class文件常量池的内容放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。
例如:类中的一个字符串常量在 class 文件中,是存放在 class 文件常量池中的;在 JVM 加载完这个类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在常量池中只会存在一份。
方法区和堆空间是类似,也是一个共享内存区,所以方法区是内存共享的。假如有两个线程都视图访问方法中的同一个类信息,而这个类还没被 JVM 加载到内存中,那么此时只允许一个线程去加载它,另外一个线程必须等待。在 HotSpot 虚拟机,Java 1.7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java 1.8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地。
元空间大小参数:
jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)
Java1.8 为什么使用元空间替代永久代,这样做有什么好处呢?
官方给出的解释是:
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。
对于方法区和永久代的区别,简单总结一下就是:
- 方法区是规范层面的东西,规定了这一个区域要存放哪些数据。
- 永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。
堆 -线程共享
堆(Heap)是JVM 所管理的最大一块内存区域,该区域唯一的目的就是存放对象实例,几乎所有对象的实例都是在堆里面分配的,因此它也是 Java 垃圾收集器管理的主要区域,有时候也叫做 GC堆。同时它也是线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问,需要考虑线程安全问题。
堆大小参数:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m
直接内存
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;
这块内存不受java堆大小限制,但受本机总内存的限制,如果这个内存溢出了,那么电脑也就司机了。可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。
从底层深入理解运行时数据区
HSDB 可视化工具
JDK1.8 启动 HSDB 的时候必须将 sawindbg.dll 复制到 jre 的对应目录下,否则启动 HSDB 查看进程时会报异常导致无法查看。

切换到 D:\Program Files\Java\jdk1.8.0_241\lib 目录下,打开命令窗口执行以下命令即可启动 HSDB 工具:
java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
接下来我们就通过执行以下一段代码,然后通过 HSDB 工具查询运行时数据区的内容:
public class JVMTest {
public static void main(String[] args) throws InterruptedException {
User zhangsan = new User(); //创建张三对象
zhangsan.setName("zhangsan");
for (int i = 0; i < 15; i++) {
System.gc(); //进行15次垃圾回收
}
User lisi = new User(); //创建李四对象
lisi.setName("lisi");
Thread.sleep(Integer.MAX_VALUE); //一直睡眠,否则线程结束就无法查看了
}
static class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
JVM 对以上代码执行整个处理流程如下:
- JVM 向操作系统申请内存。JVM 第一步就是通过参数配置或者默认参数向操作系统申请内存空间;
- JVM 获取内存后,会根据配置参数分配栈、堆以及方法区的内存大小;
- 配置好参数后,JVM 首先会执行构造器,编译器会将 .java 文件编译为 .class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法、静态变量和常量放入方法区;
- 最后执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码,此时堆内存中会创建一个 User 对象,对象的引用 zhangsan 就存放在栈中。执行其他方法时,具体操作流程,请查看前面虚拟机栈部分。

上工具
当我们运行以上代码是,线程最后一直在睡眠状态,所以我们通过可视化工具来揪出一切根源。
第一步:
在 jdk 的 bin 目录下,打开命令行执行 jps 指令,输入 Java 所有进程的ID ,刚刚我们运行的代码 JVMTest 是3968 ID,这个 ID 值先记住,下面用上。

第二步:
我们切换到 jdk 目录下的 lib 目录,然后执行 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 执行,就会弹出 HSDB 工具了,如下图:

我们可以通过点击 File ---> Attach to HotSpot process ---> 输入 JVMTest 的端口号 3968 ,点击ok 就可以看这个进程的一切了。


在下面图片中,我们可以看到 JVMTest 里面有好几个线程在跑着,我们看一下 最熟悉的 main 线程。

点击选中 main 线程 ,点击一下图标 打开 栈内存管理,就可以查看到栈帧了

首先看下面图,我们可以看到两块区域,代表的是两个栈帧信息,第一块黄色线框圈出来是 Thread.sleep 栈帧,第二块是绿色线框圈出来的是 main 栈帧。图片里面包含栈帧信息以及所在的内存地址是什么都显示出来了 0x 这些都是内存地址来的。

为什么会出现两个栈帧信息?
我们跟进一下 Thread.sleep 方法里面,可以发现,sleep 的方法是一个 native 方法, 也就是本地方法栈。到这里,是不是也就证明我们上面所说的,Hotspot 虚拟机将虚拟机栈和本地方法栈合二为一了。

点击 HSDB 工具栏下的 Tools 选中 Object Histogram 查看对象信息,由于对象信息非常的多,我们需要输入类的 全限定名称就能快速找到我们要的对象,例如下面:

双击查看这个对象都有哪些,不用看都知道是两个,一个zhangsan 一个lisi 两个对象,地址是多少都抓到了。选中对象后,点击 Inspect 可以准确看到 name 值就是 zhangsan 这个对象。

看到这里,大家有没有对 JVM 内存分布有没有多一丢丢的认识???
接下来继续查看堆的分布情况:
点击 HSDB 工具栏下的 Tools 选中 Heap parameters 查看堆的相关参数,看下面的截图有没有捕获到一些关键点或者关键词呢?

没错,就是PSYoungGen 、PSOldGen 这两部分就是新生代和老年代了,还把这两部分对应的地址范围都知道了,是一段连续的地址来的,接下来把前面的截图接下来对堆的地址范围再划分一下,然后再结合上面查看 Object 信息,分别找到 zhangsan 和 lisi 的引用地址,代码中,由于我们创建 zhangsan 这个对象之后,然后循环15次 执行垃圾回收,zhangsan 没有被回收掉,所以 张三 被划分到了老年代了,而后面创建的 lisi 对象没有经历过 垃圾回收,所以被划分在新生代区。

通过 HSDB 工具查看完堆内存的情况,这时候对堆的掌握有没有又深入了那么一丢丢呀,兄dei。
虚拟机优化技术
编译优化技术——方法内联
方法内联的优化行为,就是把目标方法的代码原封不动的复制到调用方法中,避免真实的方法条用而已。如以下代码:第一点是实际的代码编写,第二点是虚拟机使用方法内联的代码拷贝。
public class JVMTest {
public static void main(String[] args) throws InterruptedException {
// 1.实际代码编写
// boolean bool = max(2, 5); //调用 max 方法,入栈 (max 栈帧)
// 2. 使用方法内联优化技术
boolean bool = 2 > 5; //减少一次 栈帧 入栈
}
private static boolean max(int a, int b) {
return a > b;
}
}
栈的优化技术——栈帧之间数据的共享
在一般的模型中,两个不同栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重贴(主要体现在方法有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接共用一部分数据,无需进行额外的参数复制传递了。

上代码演示:
public class JVMTest {
public static void main(String[] args) throws InterruptedException {
JVMTest jvmTest = new JVMTest();
jvmTest.work(4); // 4 是压入 main 栈帧的操作数栈
}
public void work(int x) throws InterruptedException {
int b = (x * 2) + 5; //局部变量表有
Thread.sleep(Integer.MAX_VALUE);
}
}
执行以上代码,打开 HSDB 工具,我们可以看到绿色框住部分,那部分的线条重贴是两个栈帧数据共享。

最后借图总结:

JVM 运行的结构中,一共有两个 栈(虚拟机栈 和 本地方法栈)、 一个堆 ,一个方法区 和一个程序计数器。只有方法区和堆是内存贡献的,其他区域都是线程私有的,而且程序计算器是JVM 规范中中唯一一块没有规定任何OutOfMemoryError 异常的区域。
虚拟机优化技术一共两个要点:
- 编译优化技术——方法内联
- 栈的优化技术——栈帧之间数据共享
感谢:King老师、姜新星老师
参考文献:
https://baike.baidu.com/item/JVM/2902369#5
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1855
网友评论