JVM 基础与入门
什么是 JVM ?
JVM 全称 Java Virtual Machine 就是 Java 虚拟机 的意思。所谓虚拟机,就是通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统;而 Java 虚拟机,则是专属于 Java 的计算系统,也就是说,Java 虚拟机是一个虚构的计算机。
JVM 的整体知识:
- 内存结构
- 类加载
- 类文件结构
- 监控工具
- 执行引擎
- JVM 自身优化技术
- 性能调优
- 垃圾回收
基础概念
Java 从编译到执行的过程:从我们编写的 **.java 代码,到通过编译工具生成的 **.class 文件,然后在通过 JVM 里的 ClassLoader (类加载器)调用解释 class 文件所需要的 Java 类库,再进行相应的操作(字节码解释器或 JIT 编译器,一般都是通过字节码解释器进行操作,JIT 编译器则在即时编译中会用到),到执行引擎中执行,输出到系统中。
由上图可知,JVM 是属于 JRE 的,而 JRE 则属于 JDK 。
- JDK 全称是 Java Development Kit (Java 开发工具包)其中有许多相关的工具,如 javac 则是编译工具
- JRE 全称是 Java Runtime Environment (Java 运行时环境)其中有解释 class 文件所需要的 Java 内库
- JVM即 Java 虚拟机,包含了 ClassLoader(类加载器)、运行时数据区、执行引擎、本地方法接口、垃圾回收。
主流的虚拟机
可以在当前操作系统的终端上,通过 java -version 命令查询当前 JDK 的相关信息
常用的就是 Oracle 公司的 HotSpot ,其他一些主流的虚拟机如下:
- Jrockit:是 BEA 公司开发的虚拟机,曾号称“世界上最快的虚拟机”,后被 Oracle 公司收购整合到 HotSpot 中。
- J9:是 IBM 公司的,主要用在 IBM 产品上。
- TaoBaoVM:是咱们国内大厂、Java界的“黄埔军校”、阿里巴巴基于 HotSpot 进行的一个深度定制版。
- zing:是 Azul Systems 公司开发的商业虚拟机(要收费的~),垃圾回收速度非常快(大概在 1 毫秒以内),属于业界标杆。
【PS:它的一个垃圾回收算法后被 Oracle 公司的 HotSpot 吸收,才有了垃圾回收速度在 10 毫秒之内的 ZGC。】
更多关于虚拟机的信息可以到知乎里的目前主流的 Java 虚拟机有哪些? - RednaxelaFX的回答 - 知乎中了解相关信息。
跨平台与跨语言
JVM 的跨平台性:由于是一个通过软件模拟的完全隔离环境的计算机系统,所以它可以在不同的操作系统(如 Linux、Windows、MacOS、Android 等)上执行。即同一个 Java 代码在不同操作系统上所呈现的最终效果一致。
不同的操作系统有对应版本的 JDK,可以到官方下载地址中找到对应的 JDK 进行下载使用。
JVM 的语言无关性(跨语言):由于 JVM 只识别字节码,并不和包括但不仅限于 Java 的其他编程开发语言进行关联识别,所以理论上来说,任何可以编译成 .class 文件的语言都可以在编译后通过 JVM 运行到平台上。

【个人认为,如果想开发一个语言的话,只需要开发一套对应规范的语言规则,以及与之相对应的编译工具,能将其转换成对应的 .class 文件的话,应该就能开发出一个简单的基于 JVM 实现的编程语言了。】
运行时数据区(JVM 的内存区域)
运行时数据区:JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域(内存虚拟化)。
作用:为了更好的管理内存。
事例:Java 的自动化内存管理。
实际上,除了运行时数据区,还有直接内存。所以统称 JVM 的内存区域

线程共享区
跟随虚拟机的启动而存在,被所有线程共享,且只有一份。
线程共享区中包含方法区和堆。
方法区(JDK 1.7 称之为永久代,JDK 1.8 称之为元空间)
- 方法区是 JVM 对内存的逻辑划分,用于存放已经被虚拟机加载的类信息。
- 方法区中包含运行时常量池,用于存放 .class 文件编译时生成的各种字面量以及符号引用。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中如有多个相同的字符串,在运行时常量池中只会存在一个。
堆
- 堆是 JVM 所管理的内存区域中最大的一块,主要用于存储对象和数组。
- 另外垃圾回收,就是在这上面操作的。
线程私有区
跟随线程启动而存在,一个线程拥有单独的一份内存区域。
线程私有区中包含虚拟机栈、本地方法栈、程序计数器三种。
虚拟机栈
虚拟机栈的生命周期与线程一致。
数据结构:先进后出(FILO)
作用:在运行时,存储当前线程运行方法是所需要的数据、指令和返回地址。
虚拟机栈的大小默认为 1MB,可通过参数 -Xss 调整大小,各个操作系统的默认大小有所不同,具体可以通过官方文档进行查看。
由于虚拟机栈限制了大小,所以如果操作不当,可能导致栈溢出 java.lang.StackOverflowError。
如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出内存溢出 java.lang.OutOfMemoryError。
栈帧
每个方法在执行时,都会创建一个栈帧。一个方法从调用直至执行结束,就映射着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧通过四个区域存储方法的相关信息:
局部变量表
- 存储局部变量(方法参数和方法内部定义的变量)的表,主要存放 Java 的八大基础数据类型,如果是 Object 类型的话,只是存放它的一个引用,实际对象在共享区域(堆)。
操作数栈
- 先进后出的数据结构,用来操作数据的临时数据存储区,其最大深度在编译时已经确定。
- 操作数栈的本质是 JVM 执行引擎的一个工作区,也就是说方法在执行的时候,才会对操作数栈进行操作,如果代码不执行,则操作数栈是空的。所以,在一个方法刚开始的时候,操作数栈是空的。
操作系统:CPU + 缓存 + 内存
JVM (模拟的操作系统):执行引擎 + 数据栈 + 栈、堆- 理论上来说,同一个虚拟机栈中的两个栈帧是相对独立的。但是在绝大部分的虚拟机实现中,会将前一个入栈的栈帧中的操作数栈和后一个入栈的栈帧中的局部变量表进行重叠,这样会使方法在进行调用的时候,共用一部分数据,无须进行额外的参数复制和传递。
动态链接
- Java 动态语言的特性。
- 每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
- 通过运行时常量池的符号引用(指向堆),完成将符号引用转化为直接引用。符号引用和直接引用在运行时进行解析和链接的过程。
完成出口(返回地址)
- 正常(通过调用程序计数器中的地址作为返回)
- 方法正常执行时,如有返回值,则将其结束的值返回给上层方法,经过调整后指向方法调用指定后面的一条指令,继续执行上层方法;如无返回值,则直接回到上层的方法调用中。
- 异常
- 通过异常处理表来确定,如果在本方法中的异常表中没有匹配到对应的异常处理器,就会导致方法退出。一个方法如果是异常退出,则不会给它的上层调用者产生任何返回值的。
- 正常(通过调用程序计数器中的地址作为返回)
单个方法运行时,字节码执行过程解析
首先定义一个方法,写一个简单的 Java 代码:
public ex1;
public class Person {
public int work() throws Exception {
int x = 3;
int y = 5;
int z = (x + y) * 10;
return z;
}
public static void main(String[] args) throws Exception {
Person person = new Person();
person.work();
}
}
通过 javac xxxx.java 将编译成 class 文件,然后通过 javap -c xxxx.class 进行反汇编查看字节码内容。
相关的字节码助记符请点击这里进行查询。
本地方法栈
本地方法栈是 JVM 使用到的 Native 方法相关操作。
程序计数器
程序计数器是一块较小的内存区域,用于存放当前线程执行的字节码的行号(地址)。
由于 JVM 的多线程是通过时间切片的方式来实现的,所以为了确保线程切换后能回到正确的指令上,每个线程都会有一个独立的程序计数器,各个线程之间互不影响,所以它是线程安全的。
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
另外,如果遇到 Native 方法,由于该方法不是由 JVM 具体执行,所以程序计数器不需要记录,这个是因为在操作系统层面也有一个程序计数器,这个程序计数器会记录本地代码的执行的地址,所以在执行 Native 方法时,JVM 中的程序计数器的值为空。
【PS:程序计数器是 JVM 中唯一一个不会出现 OOM 异常的内存区域。】
直接内存
JVM 在启用时,会向操作系统申请一块内存进行数据存储,而没有申请的操作系统剩余的内存叫做直接内存,也叫堆外内存。
可以借助一些工具对直接内存进行操作。
它不是 JVM 运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。
如果使用了 NIO 会频繁操作直接内存,在 Java 堆内,也可以通过 DirectByteBuffer 对象来直接引用并操作。