JVM规范基本概念
- 前言
- [从系统工程师角度学习 JVMS:Java 源码、Class 文件、JVM 执行引擎与本地机器码](#从系统工程师角度学习 JVMS:Java 源码、Class 文件、JVM 执行引擎与本地机器码)
-
- 一、学习目标与核心结论
- [Java 源码(.java)](#Java 源码(.java))
- [Class 文件(.class)](#Class 文件(.class))
- [JVM 运行时](#JVM 运行时)
- [CPU + 操作系统 + 硬件资源](#CPU + 操作系统 + 硬件资源)
- 二、理解四个核心概念及其边界
-
- [1. Java 源码:描述程序的语言级行为](#1. Java 源码:描述程序的语言级行为)
- [2. Class 文件:Java 编译器与 JVM 之间的二进制接口](#2. Class 文件:Java 编译器与 JVM 之间的二进制接口)
- [3. JVM 执行引擎:负责实现字节码规定的语义](#3. JVM 执行引擎:负责实现字节码规定的语义)
- [4. 本地机器码:最终由处理器执行的指令](#4. 本地机器码:最终由处理器执行的指令)
- [三、结合 Java 源码,完整追踪一次编译与执行](#三、结合 Java 源码,完整追踪一次编译与执行)
-
- [1. Java 源码:`AddDemo.java`](#1. Java 源码:
AddDemo.java) - [2. 编译为 Class 文件](#2. 编译为 Class 文件)
- [3. 使用 `javap` 检查字节码](#3. 使用
javap检查字节码)
- [1. Java 源码:`AddDemo.java`](#1. Java 源码:
- [四、JVM 如何从 Class 文件进入执行引擎](#四、JVM 如何从 Class 文件进入执行引擎)
-
- [1. 类加载:把二进制定义引入 JVM](#1. 类加载:把二进制定义引入 JVM)
- [2. 运行时数据区:字节码需要的执行环境](#2. 运行时数据区:字节码需要的执行环境)
- [3. 解释执行与 JIT 编译:两条可以协作的执行路径](#3. 解释执行与 JIT 编译:两条可以协作的执行路径)
- [五、结合 OpenJDK 源码理解各层职责](#五、结合 OpenJDK 源码理解各层职责)
-
- [1. Class 文件解析:`classFileParser.cpp`](#1. Class 文件解析:
classFileParser.cpp) - [2. 解释器:`templateInterpreter.cpp`](#2. 解释器:
templateInterpreter.cpp) - [3. JIT 编译:从字节码语义到机器指令](#3. JIT 编译:从字节码语义到机器指令)
- [1. Class 文件解析:`classFileParser.cpp`](#1. Class 文件解析:
- 六、如何在实际环境验证这条链路
-
- 第一步:查看编译产物
- [第二步:禁用 JIT,观察解释执行](#第二步:禁用 JIT,观察解释执行)
- [第三步:查看 JIT 编译事件](#第三步:查看 JIT 编译事件)
- 七、从系统工程师视角形成最终学习总结
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
从系统工程师角度学习 JVMS:Java 源码、Class 文件、JVM 执行引擎与本地机器码
一、学习目标与核心结论
学习《Java 虚拟机规范》(JVMS, Java Virtual Machine Specification),不能只停留在理解 Java 代码如何运行,而应建立从编译器、二进制文件、虚拟机运行时到 CPU 指令执行的完整技术链路。
本文以 Java SE 26 版 JVMS 为主要规范依据,结合 OpenJDK/HotSpot 实现源码和可运行示例进行分析。JVMS 是针对抽象虚拟机的规范,并不规定 JVM 必须采用哪种内部实现;解释执行、JIT 编译、机器码生成等实现策略,可以在满足规范语义的前提下由虚拟机实现者决定。
学习过程中需要始终区分两个层次:
- 规范层:规定 Class 文件的格式、字节码指令的语义、运行时数据区、方法调用和异常处理等行为。
- 实现层:研究 HotSpot 如何解析 Class 文件、执行字节码、编译热点方法、生成机器码,以及如何协调操作系统和 CPU。
核心链路如下:
Java 源码(.java)
开发者编写的高级语言程序
javac:词法、语法、类型检查等编译工作
Class 文件(.class)
平台无关的二进制结构、常量池、方法字节码及属性等
类加载、验证、链接、初始化;解释执行或编译执行
JVM 运行时
解释器
按字节码语义执行
JIT 编译器
将热点代码编译为机器码
两条路径可以共存,并非所有方法都会经历完全相同的执行流程。
机器指令在具体处理器上执行
CPU + 操作系统 + 硬件资源
执行本地指令、访问内存、处理分支和调用等
需要特别注意:这是一张逻辑关系图,不表示每个 Java 程序都必须依次经过所有阶段。JVMS 明确允许实现者在加载时或执行期间将字节码转换为宿主 CPU 的指令,也允许采用其他实现方式。
二、理解四个核心概念及其边界
1. Java 源码:描述程序的语言级行为
Java 源码由开发者编写,例如:
java
int result = add(20, 22);
这段代码描述的是高级语言操作,不直接描述 CPU 寄存器、机器指令编码或调用约定。
通常,javac 将 Java 源文件编译为 JVM 能够识别的 Class 文件。编译器会进行语法和类型检查,并生成适合目标 Class 文件版本的二进制结构。
从系统工程师角度,需要关注三个问题:
- 语义:程序应该产生什么结果?这由 Java 语言规范及相关 API 契约约束。
- 编译产物:源码中的一个操作会被编译成哪些 JVM 字节码?
- 运行时优化:JVM 后续是否会内联方法、消除冗余计算或者改变机器码布局?
不能仅凭源码推断最后执行了哪些 CPU 指令。
2. Class 文件:Java 编译器与 JVM 之间的二进制接口
Class 文件是理解 JVMS 最关键的中间层。
JVMS 第 4 章规定了 Class 文件结构。它采用与具体硬件和操作系统无关的二进制格式,文件中的多字节数据按大端序存储;一个 Class 文件包含一个类、接口或模块的定义。类也可以由类加载器动态生成,并不一定来自磁盘上的 .class 文件。
一个普通的 Class 文件主要包含以下部分:
| 部分 | 主要职责 | 系统分析重点 |
|---|---|---|
magic |
标识 Class 文件格式,值为 0xCAFEBABE |
文件是否具有正确格式 |
minor_version、major_version |
声明 Class 文件版本 | 目标 JVM 是否支持该版本 |
constant_pool |
存储类、方法、字段、字符串等常量和符号引用 | 常量解析、动态链接 |
access_flags |
描述类的访问及特定属性 | 类定义的元信息 |
fields |
描述字段 | 类型、名称、访问标志 |
methods |
描述方法 | 方法签名、访问标志、代码属性 |
attributes |
存放代码及附加元数据 | Code、StackMapTable、调试信息等 |
该布局和字段定义来自 JVMS 第 4 章。
其中最值得关注的是 Code 属性。对于具有普通 Java 方法体的方法,它保存 JVM 字节码以及执行所需的辅助信息,包括:
max_stack:方法执行过程中操作数栈所需的最大深度。max_locals:方法局部变量表所需的槽位数量。code:方法的字节码指令序列。exception_table:异常处理表。attributes:例如栈映射帧、行号等附加信息。
抽象结构可以简化为:
ClassFile
├── magic / version
├── constant_pool
├── access_flags / this_class / super_class
├── fields[]
├── methods[]
│ ├── method_info
│ └── Code
│ ├── max_stack
│ ├── max_locals
│ ├── code[]
│ ├── exception_table[]
│ └── attributes[]
└── attributes[]
学习总结:Class 文件不是 Java 源码的简单压缩版,也不是 CPU 机器码。它是具有严格格式和语义约束的 JVM 二进制接口。
这也解释了为什么不同语言编译器可以生成 JVM 字节码:JVM 关注的是符合规范的 Class 文件及其语义,而不是程序最初由哪一种高级语言编写。
3. JVM 执行引擎:负责实现字节码规定的语义
JVMS 描述的是一台抽象虚拟机。它规定程序执行结果必须满足什么语义,但不会要求每个实现都使用同一种解释器、编译器或内存布局。
在 HotSpot 中,可以重点区分以下几个组成部分。
| 组成部分 | 职责 | 关键问题 |
|---|---|---|
| 类加载和链接机制 | 读取、验证、准备并解析类 | 非法 Class 文件如何被发现? |
| 运行时数据区 | 为执行提供栈、堆、常量池等运行时资源 | 对象和方法执行状态在哪里? |
| 解释器 | 解释执行 JVM 字节码 | 指令如何被分派和执行? |
| JIT 编译器 | 将热点方法或代码片段编译成本地代码 | 如何利用运行时信息优化? |
| 运行时支持机制 | 提供对象分配、异常处理、GC、同步和运行时调用等支持 | 编译代码怎样正确参与 JVM 运行? |
注意:这些是帮助分析 HotSpot 的实现划分,不应理解为 JVMS 要求所有 JVM 都必须具有完全相同的软件组件。
理解执行引擎时,有一个容易忽略的细节:
解释执行并不意味着 CPU 直接执行 JVM 字节码。
JVM 字节码是一套面向抽象虚拟机的指令集,而 CPU 执行的是当前处理器架构定义的机器指令。HotSpot 的解释器本身也是由宿主 CPU 可以执行的本地代码实现的,它根据当前字节码执行相应的处理逻辑。相关实现可以从 OpenJDK 的 templateInterpreter.cpp 入手。
4. 本地机器码:最终由处理器执行的指令
本地机器码(native machine code)是面向具体处理器架构生成的指令序列,例如 x86-64 或 AArch64。
它和 JVM 字节码不是同一种指令集。
假设 JVM 字节码中有:
iload_0
iload_1
iadd
ireturn
这描述了 JVM 抽象执行过程:取得两个整型操作数、相加并返回结果。
JIT 编译器可能将其转换成类似于以下的处理器级操作:
; 示意性伪汇编,不对应某个确定的 JVM 编译结果
; 将两个整数参数放入处理器寄存器
; 使用整数加法指令执行相加
; 按当前平台的调用约定返回结果
具体的机器指令、寄存器分配、参数传递、栈帧和返回约定,取决于目标架构、操作系统、编译优化和调用场景,不能仅凭字节码固定下来。
而且,JIT 不要求保持字节码与机器指令的一一对应关系。编译器可能内联方法、消除重复计算、合并操作或者把中间结果一直保存在寄存器中。
因此,排查性能问题时,必须区分:
- 字节码层:编译器输出的 JVM 指令是什么?
- JVM 实现层:解释器或 JIT 如何处理这些指令?
- 硬件执行层:实际生成了什么机器指令,指令如何影响寄存器、缓存和内存访问?
三层观察的是同一个程序在不同抽象层次上的表现。
三、结合 Java 源码,完整追踪一次编译与执行
下面使用一个简单示例,观察源代码如何变成字节码,再分析 JVM 如何执行它。
1. Java 源码:AddDemo.java
java
public final class AddDemo {
// 定义一个静态方法。
// 在 Java 源码层面,它接受两个 int 参数,并返回它们的和。
static int add(int a, int b) {
return a + b;
// 注意:这里写的是 Java 加法表达式,
// 并没有指定必须使用哪条 CPU 加法指令。
}
public static void main(String[] args) {
// 调用 add 方法。
// javac 通常会将该调用编译为 invokestatic 字节码。
int result = add(20, 22);
// 获取标准输出对象并调用 println。
// 字节码层面通常涉及 getstatic、iload 和 invokevirtual。
System.out.println(result);
}
}
从源码角度,这段程序只表达了两个整型相加并打印结果。它没有声明 CPU 寄存器、指令地址或本地调用约定。
2. 编译为 Class 文件
执行:
shell
javac -g AddDemo.java
生成 AddDemo.class。
其中:
javac负责将源码编译为 JVM 可识别的 Class 文件。-g请求生成调试信息,便于查看源码行号和局部变量信息。- 生成的字节码仍然不是针对 x86-64 或 AArch64 的 CPU 指令。
3. 使用 javap 检查字节码
执行:
shell
javap -c -v -p AddDemo
其中 -c 用于反汇编方法字节码,-v 用于查看详细 Class 文件信息,-p 用于显示包括私有成员在内的类成员。
add 方法的关键输出通常类似于:
static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
逐条分析:
| 偏移 | 字节码 | 含义 |
|---|---|---|
| 0 | iload_0 |
将第 0 个局部变量中的整型值压入操作数栈 |
| 1 | iload_1 |
将第 1 个局部变量中的整型值压入操作数栈 |
| 2 | iadd |
弹出两个整型操作数并将和压入操作数栈 |
| 3 | ireturn |
返回整型结果 |
这是对 JVM 抽象执行语义的描述,而不是对物理寄存器操作的直接描述。iload、iadd 与 ireturn 的语义由 JVMS 指令集章节定义。
main 方法的关键指令通常类似于:
0: bipush 20
2: bipush 22
4: invokestatic #... // Method add:(II)I
7: istore_1
8: getstatic #... // Field java/lang/System.out
11: iload_1
12: invokevirtual #... // Method java/io/PrintStream.println:(I)V
15: return
这里的 #... 只是示意,实际的常量池索引由编译结果决定。
重要指令说明:
bipush:将一个带符号字节表示的立即数扩展为整数并压入操作数栈。invokestatic:通过方法符号引用调用静态方法。istore_1:把结果写入局部变量表的第 1 个槽位。getstatic:读取静态字段,这里是System.out。invokevirtual:执行实例方法调用,这里用于调用println。return:从返回类型为void的方法正常返回。
关键结论:Java 源码里的方法调用,到 Class 文件中表现为 JVM 的方法调用指令和符号引用。符号引用解析、目标方法选择以及最终的底层调用方式,是后续运行时处理的问题。
需要注意,以上输出是用于理解机制的典型形式;实际指令细节应以当前 JDK 编译得到的 Class 文件为准。
四、JVM 如何从 Class 文件进入执行引擎
从系统工程师角度,理解程序运行过程时,应当把类加载与链接、运行时数据管理、字节码执行和机器码生成分开分析。
1. 类加载:把二进制定义引入 JVM
典型流程可以抽象为:
读取 Class 文件或其他字节来源
│
▼
加载(Loading)
│
▼
链接(Linking)
├─ 验证(Verification)
├─ 准备(Preparation)
└─ 解析(Resolution)
│
▼
初始化(Initialization)
│
▼
方法执行
其中,链接包含验证、准备和解析。解析的具体时机允许采用不同策略,并不意味着所有符号引用都必须在类刚加载时一次性解析完成。
从排障视角,需要分清典型问题所属的阶段:
| 问题 | 重点排查方向 |
|---|---|
ClassFormatError |
Class 文件格式、版本或结构 |
VerifyError |
字节码约束、类型安全及验证规则 |
NoClassDefFoundError |
类定义的可用性,以及此前加载或初始化失败的情况 |
NoSuchMethodError |
方法符号引用与运行时类定义是否兼容 |
ExceptionInInitializerError |
类初始化过程中的异常 |
这些异常并不都由同一个组件抛出,但把错误定位到正确的阶段,可以明显缩小排查范围。
2. 运行时数据区:字节码需要的执行环境
JVMS 第 2 章还规定了抽象运行时数据区。学习时,应特别关注以下结构:
| 运行时结构 | 主要作用 | 系统工程师需要理解什么 |
|---|---|---|
pc 寄存器 |
跟踪当前线程正在执行的 JVM 指令位置 | 字节码执行位置及调用返回 |
| JVM 栈 | 管理方法调用帧 | 调用深度、栈帧和局部变量 |
| 局部变量表 | 保存方法参数及局部变量槽位 | 参数传递、变量生命周期 |
| 操作数栈 | 为字节码提供操作数和中间计算结果 | iadd 等指令如何消费与产生数据 |
| 堆 | 为对象及数组提供运行时存储空间 | 对象布局、GC、分配与内存访问 |
| 方法区 | 保存类相关的运行时结构 | 类元数据与实现相关的存储方式 |
| 运行时常量池 | 支持常量及符号引用等信息 | 字段、方法引用和动态链接 |
| 本地方法栈 | 为本地方法执行提供相关运行时支持 | JNI、线程与本地运行环境 |
这些名称描述的是 JVMS 的抽象模型。不能简单地把规范中的每个结构等同于 HotSpot 中某个固定的内存地址、C++ 类或物理内存分区。
例如,解释器执行 iadd 时,JVMS 的语义可以描述为从操作数栈取得两个整数并压入计算结果;JIT 编译后的代码则可能直接利用 CPU 寄存器完成运算,未必真的在物理内存中构造一个与抽象操作数栈逐项对应的结构。
学习总结:规范定义的是可观察的行为和必须满足的约束,实现可以采用不同的数据结构与优化方式,只要保持规定的语义。
3. 解释执行与 JIT 编译:两条可以协作的执行路径
在常见的 HotSpot 分层编译模式下,程序可能经历以下过程:
Class 文件
│
▼
类加载与链接
│
▼
方法执行
│
┌─────┴─────┐
▼ ▼
解释执行 运行时编译
│ │
│ 收集运行信息
│ │
│ C1 / C2 编译
│ │
└─────┬─────┘
▼
编译机器码执行
│
▼
运行时监测及必要的去优化
这是一种常见的实现路径,不是所有 JVM 都必须遵循的固定流程。
解释执行:解释器读取字节码并执行相应操作。它的指令处理逻辑本身也是由宿主机器指令实现的。
JIT 编译:编译器利用静态信息和运行时收集的信息,优化热点方法或循环,并生成适用于目标处理器的本地机器码。
去优化(Deoptimization):当编译时依赖的某些假设不再成立,或者运行时需要恢复更适合解释执行的状态时,JVM 可以使执行从已编译代码回到解释器等适当执行状态。
因此,不应把"一次方法执行"简单理解为"先解释完毕,再完整编译一次"。在真实的 JVM 中,同一方法的执行路径可能随运行时状态而变化。
五、结合 OpenJDK 源码理解各层职责
下面列出适合从源码入手的几个关键位置。阅读时,应当区分 JVMS 的强制语义要求与 OpenJDK/HotSpot 自己选择的实现方式。
1. Class 文件解析:classFileParser.cpp
源码位置:src/hotspot/share/classfile/classFileParser.cpp
OpenJDK 在解析 Class 文件时,需要检查并处理二进制结构,读取常量池、字段、方法以及相关属性。
源码中的部分定义如下,右侧注释为学习说明:
cpp
#define JAVA_CLASSFILE_MAGIC 0xCAFEBABE
// 学习点:Class 文件以固定魔数标识其文件格式。
#define JAVA_MIN_SUPPORTED_VERSION 45
// 学习点:这是该源码版本中使用的最低支持版本定义。
// 实际支持范围应以正在分析的 JDK 版本和实现配置为准。
上述定义可以在 OpenJDK 的 classFileParser.cpp 中找到。
阅读该文件时,重点追踪:
- Class 文件字节流是如何读取的。
- 常量池中的符号与引用信息是如何解析的。
- 字段和方法的元数据如何构建。
Code、StackMapTable等属性如何参与类定义处理。- 解析和格式检查与后续字节码验证之间有什么区别。
需要注意,完整的验证流程不仅涉及 ClassFileParser,还需要结合 HotSpot 的验证器实现和 JVMS 第 4 章的约束一起分析。
2. 解释器:templateInterpreter.cpp
源码位置:src/hotspot/share/interpreter/templateInterpreter.cpp
该文件可以帮助理解 HotSpot 如何初始化解释器、生成解释器代码以及设置字节码分派相关的数据结构。
以该文件中的初始化过程为例,其关键步骤可以概括为:
cpp
// 教学性摘录:省略非关键实现细节。
AbstractInterpreter::initialize();
// 注释:初始化解释器相关的基础结构。
TemplateTable::initialize();
// 注释:初始化模板解释器使用的字节码处理模板。
// 后续由解释器生成器生成相应解释器代码。
_active_table = _normal_table;
// 注释:选择正常执行使用的字节码分派表。
这段摘录用于指出源码阅读的方向,不是完整可编译的独立代码。实际的代码生成过程应以具体 JDK 版本的实现为准。
从这里要建立的认识是:
HotSpot 解释器不是简单地把每个字节码当作一段独立的 CPU 指令执行,而是通过本地代码中的指令处理和分派逻辑来实现 JVM 字节码的语义。
3. JIT 编译:从字节码语义到机器指令
HotSpot 中可以重点阅读:
| 源码位置 | 阅读重点 |
|---|---|
src/hotspot/share/compiler/compileBroker.cpp |
编译请求与编译任务的协调 |
src/hotspot/share/c1/ |
C1 编译器实现 |
src/hotspot/share/opto/compile.cpp |
C2 编译流程与优化相关逻辑 |
src/hotspot/share/opto/output.cpp |
C2 编译结果的代码生成与输出相关逻辑 |
研究时建议按照这条主线追踪:
编译请求
↓
编译器分析方法
↓
构建并优化中间表示
↓
指令选择和寄存器分配
↓
生成目标架构机器码
↓
纳入运行时可执行代码的管理
这是帮助理解编译过程的逻辑图,并不代表每个 HotSpot 编译器使用完全一样的内部流水线。
学习时要重点观察:
- 哪些信息来自 Class 文件,哪些来自运行时 profile。
- 字节码经过何种分析后被转换为内部表示。
- 内联、常量传播、死代码消除等优化如何改变生成代码。
- 目标 CPU 的指令选择及寄存器分配如何影响最终结果。
- 编译代码如何与异常处理、GC、安全点和去优化机制协作。
对系统工程师而言,最重要的不是机械背诵所有编译器类名,而是掌握从字节码语义到实际机器指令之间的转换和优化过程。
六、如何在实际环境验证这条链路
建议在本地安装的 JDK 上依次执行下面的操作。
第一步:查看编译产物
shell
javac -g AddDemo.java
javap -c -v -p AddDemo
观察常量池、方法信息、Code 属性和字节码指令。不要只看反汇编结果中的指令名称,还要结合偏移和操作数栈语义理解。
第二步:禁用 JIT,观察解释执行
shell
java -Xint AddDemo
对于普通 Java 方法,-Xint 可用于观察解释执行路径。它并不意味着 JVM 及其所调用的本地库不执行机器码。
第三步:查看 JIT 编译事件
shell
java -XX:+PrintCompilation AddDemo
PrintCompilation 用于观察编译事件。简单示例可能不会触发 AddDemo 中方法的 JIT 编译,因为执行次数不足以使其成为热点。
若要进一步验证 JIT,可以修改程序,使 add 在长循环中被反复调用,再观察 PrintCompilation 的输出。具体是否编译、何时编译以及最终生成何种代码,都受编译策略、运行时数据和 JVM 参数影响。
如果希望观察机器指令级结果,还可以研究适用于目标 JDK 的汇编打印和反汇编工具。需要明确的是,javap -c 输出的是 JVM 字节码,不是 JIT 生成的 CPU 机器码。汇编输出工具及其可用性可能依赖 JVM 构建、诊断选项和相应反汇编支持。
七、从系统工程师视角形成最终学习总结
通过上述内容,应当形成以下认识。
第一,Java 源码和 Class 文件是不同的抽象层。 源码描述程序的语言级意图,Class 文件以严格的二进制格式表达类结构和 JVM 字节码。Class 文件版本、常量池、方法描述符和属性,是研究编译产物与运行时行为的基础。
第二,JVMS 规定语义,而不是规定唯一实现。 同一组字节码可以由不同 JVM 实现执行。解释器、JIT、对象布局、垃圾回收策略以及具体的机器码生成方式,都可以存在实现差异,但必须保持规范要求的行为。
第三,JVM 字节码不是 CPU 机器码。 字节码定义抽象执行操作;解释器使用本地代码处理这些操作,JIT 则可以将热点代码转换成针对目标 CPU 的机器指令。两者都最终依赖宿主硬件执行本地指令。
第四,性能分析必须跨层验证。 javap 可以分析字节码,但不能直接证明最终生成了哪些机器指令。解释器源码可以解释字节码分派的实现,编译器源码可以解释优化与代码生成,而机器码观测则用于进一步验证寄存器、指令序列和调用路径。只有综合这些层次,才能分析方法内联、运行时优化及硬件执行成本。
第五,正确的研究方法是规范与实现对照。 建议先掌握 JVMS 第 2 章的运行时结构、第 4 章的 Class 文件格式、第 5 章的加载与链接,以及第 6 章的指令语义;再结合 OpenJDK 的类文件解析器、解释器和 JIT 编译器逐层追踪。
最终要建立的核心模型是:
Java 源码描述程序意图,Class 文件定义 JVM 可以识别的程序表示,JVMS 规定其抽象执行语义,而具体 JVM 通过解释执行、JIT 编译及运行时支持机制,将这些语义落实到宿主机器的实际执行过程。