虚拟机2-字节码
# 字节码
字节码是什么?它到底包含什么信息?为什么它对Java这么重要?接下来,我会用简单的方式带大家了解这些问题。
首先,什么是字节码?简单来说,字节码就是Java源代码经过编译器(javac)处理后生成的二进制数据。比如我们写了个Java文件,编译后得到一个.class文件,这就是字节码文件。不过,字节码不一定非得是文件形式,它也可以是从网络传来的二进制数据流,只要符合JVM的规范,就能被加载和执行。所以,别把字节码局限在“文件”上,它更像是一种数据格式。
那字节码有什么特点呢?我总结了三点。第一,它是跨平台的关键。JVM通过读取字节码,把里面的指令翻译成不同操作系统和硬件能懂的机器指令。比如同一个.class文件,在Windows和Linux上跑,JVM会生成不同的机器指令,但结果是一样的,这就是Java“一次编写,到处运行”的秘密。第二,字节码不只属于Java。现在JVM支持多种语言,像Kotlin、Scala,只要编译器生成的字节码符合规范,JVM都能处理。第三,字节码是个结构紧凑的二进制流,格式固定,要求严格。它的内容和规则都以数据结构的方式定义好了。
字节码都是怎么编写的?
接下来,我们重点看两件事:字节码的组成结构和字节码指令。先说组成结构。想象一下,字节码像个精心打包的包裹,里面装了什么?有类的基本信息,比如名字、父类、接口;还有方法、字段的具体描述;甚至还有常量池,存着代码里用到的数字、字符串等等。这些信息排列得井然有序,JVM一看就知道怎么用。
再说字节码指令。指令是字节码的灵魂,告诉JVM该干什么。比如“iadd”是加法,“invokevirtual”是调用方法。这些指令很底层,像汇编语言,但JVM能把它们转成高效的机器码。理解指令,就能明白代码怎么一步步跑起来的。
总结一下,字节码是Java程序的中间产物,它不依赖具体平台,靠JVM翻译执行。学习字节码,我们要搞清楚它的结构和指令,这是理解JVM工作原理的第一步。
# 字节码的组成
从魔法数字开始
正式开讲字节码,先来聊聊它的“家底”——组成结构。说白了,就是搞清楚字节码里都装了些什么东西。字节码数据总共分八个部分:魔数、文件版本、常量池、访问标识、类索引与接口索引、字段表、方法表和属性表。
# 先聊魔数
你们知道魔数是啥不?其实它就像文件的“身份证”,专门用来告诉系统“我是啥类型”。Java 字节码的魔数特别有意思,叫 CAFEBABE,十六进制下就是 CA FE BA BE。JVM 每次加载 .class 文件时,都会先瞅一眼这前四个字节,看看是不是“老熟人”——标准的 Java 字节码。如果不是,它就直接摆手:“这谁啊?不认识,拒载!” 比如,JPEG 文件的魔数是 FFD8FF,PNG 是 89504E47,每种文件都有自己的“身份证号”。为啥要搞个魔数?简单,提高可靠性!文件扩展名随便改都没用,但二进制数据可没那么好糊弄。
想看看魔数长啥样?例如加载 ConstantPoolSimple.class 文件,最前面四个字节就是 CA FE BA BE。JVM 一瞧:“嘿,是自己人!”再打开个 2.png,前四个字节变成 89504E47,系统立马认出这是 PNG 图片。所以说,魔数就是文件类型的“门牌号”。
# 再说文件版本
文件版本占四个字节,5-6 是次要版本号,7-8 是主要版本号,主要版本号直接对应 JDK 大版本。比如,十六进制 0034 转成十进制是 52,那就是 JDK 1.8。回到 WinHex,看看 ConstantPoolSimple.class 的 5-8 字节,是 0000 0031,0031 转十进制是 49,说明这文件是用 JDK 1.5 编译的。日常开发中,主要看这个版本号,常见的比如 49(JDK 1.5)、50(1.6)、51(1.7)、52(1.8)。
没工具咋办?别急,Java 自带了个“神器”——javap。在命令行定位到字节码目录,敲一行 javap -v -l -c ConstantPoolSimple.class,回车,屏幕上会显示“major version: 49, minor version: 0”,一看就知道是用 JDK 1.5 编译的。
# 讲个常见问题
有一次我可栽了个跟头,运行程序时蹦出个错误:“Unsupported class version error”,说版本号 51.0 不支持。啥意思?就是字节码用 JDK 1.7 编译的,但当时的 JVM 版本太老,撑不住。JVM 这家伙有个脾气:低版本字节码它能兼容,高版本的它就直接说“拜拜”。咋解决?简单,换个高版本 JVM,比如 1.7 或更高。所以,看到版本号,你得立马反应出对应的 JDK 版本。
# 常量池
闲言少叙,我们继续,来聊聊字节码里的一个重要部分——常量池。常量池在字节码中占了不少篇幅,里面到底存了什么呢?简单来说,它包含三类数据:字面量、符号引用和常量池数据类型表。别急,咱们一步步拆解,保证你们听完就明白。
首先打开jclasslib,我们能发现最多的大概就是UTF8INFO,这里面存储了类的属性的名称,方法引用路径……什么都有,原来它是存储了这个.class文件的所有的文本的信息。下面我们看看它主要包含了哪些
# 1. 字面量
字面量就是代码里那些固定的值,样子大概是基本类型_info,主要包括:
String/Else-Info | Description |
---|---|
字符串 | 比如 "姓名"、"年龄",只要你在代码里写了字符串,它们就会被存进常量池。 |
final 修饰的常量 | 比如 final double tmp = 0.0;,这个 0.0 也会出现在常量池里。 |
这些数据是程序运行的基础,直接写死的数值就属于这一类。
# 2. 符号引用
符号引用听起来有点复杂,其实就是类、方法和字段的描述信息,相当于一个“索引”或“指针”,在字节码里面也能看到XXX Ref 的,他就是一些符号应用,。具体包括:
引用 | Description |
---|---|
类和接口的全名 | 比如 java.lang.String,告诉你用到了哪些类。存储了类的文本信息 |
字段的名称和类型 | 比如 name:Ljava/lang/String;,name and type 描述字段叫什么、是什么类型。 |
方法的名称和签名 | 比如 output:(Ljava/lang/String;)V,表示方法名是 output,参数是 String,返回值是 void(用 V 表示)。 |
这些信息不直接存具体数据,而是指向其他地方,起到连接作用。
# 3. 常量池数据类型表
这是用来表示数据类型的简写规则,这些简写在字节码里很常见,记住了就能快速看懂字段和方法的类型。比如:
数据类型的简写 | Description |
---|---|
I | 表示 int |
D | 表示 double |
L | 后面跟类名表示引用类型,比如 Ljava/lang/String; 就是 String 类型。 |
我们还可以使用JClassLib 插件查看字节码。打开 JClassLib,找到 Constant Pool,你会看到一堆条目,比如:
- UTF8Info:存文本,比如 "苹果"。
- StringInfo:存字符串引用,指向 UTF8Info。
- DoubleInfo:存 final double 的值,比如 0.0。
# 字面量示例
- 字符串:代码里的 "苹果" 在常量池里是 StringInfo,它指向一个 UTF8Info文本信息,里面存着具体的 "苹果"。
- final 常量:final double tmp = 0.0; 对应一个 DoubleInfo,直接存 0.0。
# 符号引用示例
- 类信息:ClassInfo 列出用到的类,比如 ConstantPoolSample 和隐式引入的 java.lang.StringBuilder(字符串拼接时用到的)。
- 字段信息:FieldRefInfo 描述字段,比如 tmp,它关联到类名和字段的名称类型(NameAndType),类型用 D 表示 double。
- 方法信息:MethodRefInfo 描述方法,比如 output1,签名是 (Ljava/lang/String;)Ljava/lang/String;,意思是参数和返回值都是 String。
# 总结
常量池的核心内容就是这三类:
- 字面量:字符串和 final 常量,存具体的值。
- 符号引用:类、方法、字段的描述,存“指针”信息,引用到实际的位置。
- 数据类型表:类型简写,比如 I、D、L类名;。
这些是常量池的基本条目,在 JVM 运行时会被解析。
# 类索引与访问标志
# 1. 类、父类、接口索引
什么是索引?简单说,就是指向常量池里类名、父类名、接口名的“指针”。它们告诉 JVM 这个类是谁、继承了谁、实现了啥接口。
- 类索引:当前类的全限定名(通俗的讲就是咱们的完整类名),比如
com.example.SubClass
。 - 父类索引:父类的全限定名,比如
com.example.BaseClass
。 - 接口索引集合:所有接口的全限定名集合,因为 Java 支持多接口实现。
# 小例子
假设有四个类或接口:
InterfaceA
和InterfaceB
:两个空接口。BaseClass
:抽象类,实现InterfaceA
和InterfaceB
,有个抽象方法method1
。SubClass
:继承BaseClass
,实现method1
。
用 JClassLib 查看 SubClass
的字节码:
- This Class:指向
SubClass
。 - Super Class:指向
BaseClass
。 - Interfaces:数量 0,因为它没直接实现接口。
再看 BaseClass
:
- This Class:指向
BaseClass
。 - Super Class:指向
java.lang.Object
(默认父类)。 - Interfaces:数量 2,指向
InterfaceA
和InterfaceB
。
这些索引底层都连到常量池的 ClassInfo
,再关联到具体的类名字符串(文本常量,上面是完整的类名)。
# 2. 访问标志
访问标志是一张信息表,记录类、字段、方法的权限和特性,比如 public
、final
、abstract
等。JVM 靠它决定怎么加载和访问。
- 类的访问标志:如
public
、abstract
、interface
。 - 字段的访问标志:如
public
、private
、static
、final
。 - 方法的访问标志:如
public
、synchronized
、native
。
# 小例子
拿个 ConstantPoolSample
类看看:
- 类级别:
Access Flags
显示public
,和源码一致。 - 字段级别:一个
private final
字段,Access Flags
就是private final
。 - 方法级别:
public
方法显示public
,main
方法显示public static
。
访问标志就是这么简单,清晰标注权限和特性。
# 总结
- 索引:像类的“身份证”,指向常量池,说明继承和实现关系。
- 访问标志:像“权限标签”,告诉 JVM 怎么处理类、字段、方法。
# 字段表、方法表与属性表
# 一、字节码的整体结构
首先,我们快速看一下Java字节码的组成。一个字节码文件包含八个部分:
- 魔术(Magic Number):前4个字节,通常是“CAFEBABE”,标识这是字节码文件。
- 文件版本:记录编译时的JVM版本。
- 常量池:存储所有常量数据,比如字符串、类名等,是字节码的核心。
- 访问标志:定义类或方法的访问权限,比如public、static。
- 类、父类和接口索引集合:指明类的继承关系和实现的接口。
- 字段表集合:描述类中的字段。
- 方法表集合:描述类中的方法。
- 属性表:提供辅助信息。
在下面的内容里我们聚焦后三项:字段表、方法表和属性表。他们的区别其实就是一个是类的,一个是方法的,一个是共享的,一个是私有的。
# 二、字段表(Fields Table)
字段表描述的是类或接口中声明的字段,主要包括两类:
- 类变量:用static修饰的静态字段,初学的时候,我就知道它是被整个类所共享的。
- 实例变量:普通的成员变量。
字段表记录了什么呢?主要有三点:我们用 public static final YES = "Y",一下就能理解了
- 字段名称:比如“name”。
- 字段类型:比如“String”或“int”。
- 访问标志:比如public、private。
举个例子,在我们的演示代码ConstantPoolSample中,字段表列出了name、age和tmp三个字段。比如name字段,它的类型是String,在字节码中指向常量池的“Ljava/lang/String;”,名称指向“name”。这些信息都通过常量池索引关联,逻辑清晰。
# 三、方法表(Methods Table)
方法表描述的是类中定义的方法,包括实例方法和静态方法。它的内容也很直接:
- 方法名称:比如“output”。
- 方法描述符:包括参数类型和返回值类型。
- 访问标志:比如public、static。
比如在示例中,构造方法<init>
的参数是(Ljava/lang/String;I)V,表示接收一个String和一个int,返回void。这与源代码中的构造方法完全一致。方法表还包括实例方法output和静态方法main,每项都通过常量池索引指向具体名称和类型。
# 四、属性表(Attributes Table)
属性表为字节码提供辅助信息,通常在运行时或调试时使用。它的结构是一个属性名和属性值的组合。
比如在示例中,属性表有一个“SourceFile”属性,属性名是“SourceFile”,值是“ConstantPoolSample.java”,表示源文件名。这些信息同样通过常量池索引关联,比如指向“ConstantPoolSample.java”这个字符串。
简单来说,字段表列出类的字段,方法表列出类的方法,属性表提供额外信息。这三部分与常量池紧密协作,共同构成了字节码的核心结构。
# 字节码指令
什么是字节码指令?
字节码指令是包含在Java字节码中的一系列命令,由Java编译器在编译源代码时生成,并保存在字节码文件的“方法描述”部分。它的作用很简单:告诉Java虚拟机(JVM)在程序运行时该执行什么操作。
字节码是Java跨平台特性的核心。无论在Windows、Linux还是其他系统上,同一份Java源代码编译后生成的字节码(包括其中的指令)都是完全相同的。JVM加载字节码后,会把这些平台无关的指令翻译成对应系统的底层机器指令,完成运行和计算。可以说,JVM就像一个“翻译官”,连接了字节码和具体硬件。
# 字节码指令的格式和特点
- 格式:每条字节码指令由两部分组成:
- 操作码(Opcode):一个字节(值在0-255之间),表示具体操作,总数不超过256个。
- 参数:零个或多个附加数据,具体个数取决于指令。例如,有的指令如
nop
(空操作)不需要参数,而有的如invokevirtual
需要指定方法索引。
- 特点:
- 设计简洁高效,便于JVM快速解析和执行。
- 总数有限,学习起来就像记单词,每个指令都有明确用途。
- 与Java源代码的关系:有些指令直观(如
new
),有些则抽象(如invokevirtual
调用方法)。
# 字节码指令的实际应用
来看两个例子:
invokevirtual #8
- 含义:调用常量池中第8号索引的方法。
- 假设常量池#8是
StringBuilder.append
,JVM执行这条指令时,就会调用StringBuilder
对象的append
方法,完成字符串追加。
new #6
- 含义:创建常量池中#6对应的对象。
- 如果#6是
StringBuilder
,JVM会实例化一个StringBuilder
对象,并将其放入Java堆内存中。
常量池的作用:指令中的参数(如#8
、#6
)指向常量池中的数据(如方法名、类名),增强了指令的灵活性和复用性。
# 字节码指令长什么样?
在实际字节码文件中,指令以列表形式出现。例如:
0: getstatic #2 // 获取静态字段(如System.out)
3: new #6 // 创建StringBuilder对象
6: dup // 复制栈顶值
7: invokespecial #7 // 调用构造方法(如StringBuilder.<init>)
2
3
4
JVM加载这些指令后,动态翻译成底层机器指令执行。普通开发者无需背诵,但了解它们有助于调试和优化。
# 总结
字节码指令是JVM执行Java程序的“语言”,由编译器生成,平台无关,依靠JVM翻译运行。它的总数不超过256个,格式简洁,分类清晰,是Java跨平台和高效率的关键。学习字节码指令能帮助我们更深入理解Java运行机制,提升开发能力。
# 字节码指令_与他的200多号兄弟
我们来聊聊字节码指令,这是在学习字节码时非常核心的内容。什么是字节码指令?简单来说,它就是包含在字节码里的一系列命令,告诉JVM(Java虚拟机)在运行时该做什么。字节码是由编译器从源代码生成并保存在方法描述中的,作用是平台无关——不管在Windows、Linux还是其他系统上,同一份源代码编译出的字节码都一样,指令也完全一致。
JVM加载字节码后,会读取这些指令,然后根据不同平台的底层机器语言进行翻译和执行。可以说,JVM就像个翻译官,把与平台无关的字节码转化成系统能懂的指令。至于字节码指令的数量,为了效率考虑,总数不超过256个,所以学习它并不复杂,就像背单词一样,每个指令都有自己的意思。
字节码指令的格式呢,通常是一个单词,后面可能跟参数,参数数量不固定。比如,“invokevirtual #8”,这是调用方法的指令,意思是执行常量池中8号索引位置的方法。假设8号索引指向“StringBuilder.append”,JVM读到这行就会执行追加字符串的操作。再比如“new #6”,表示实例化一个对象,6号索引可能是“StringBuilder”,于是JVM会在堆内存中创建这个对象。这些指令有些跟Java代码很像,比如“new”,但有些就不同,比如“invokevirtual”。
实际工作中,字节码指令长什么样?我们可以用工具,比如JClassLib来看。打开一个类的字节码,找到“methods”里的“code”部分,就会看到“getstatic”、“new”、“dup”这些指令。它们旁边可能有“#6”这样的索引,指向常量池里的内容,比如某个类或字符串。JVM加载这些指令后,会动态生成对应的机器指令。
对于普通Java开发者来说,不需要死记硬背这些指令。它们可以分成几类,比如加载、存储、运算、方法调用等,每类有自己的用途和格式。
# 字节码指令的分类
“指令”这个词的意思是“指导机器做什么”。在计算机领域,英文 “instruction” 翻译成“指令”,很贴切地表达了这种“指挥机器干活”的感觉。字节码指令之所以叫“指令”,是因为它本质上是对 JVM 下达的一个个具体任务。比如 iload 是“加载一个整数”,iadd 是“把两个整数相加”,就像你在指挥一个机器人:“去拿东西”“把这俩加起来”。JVM 收到这些指令,就按顺序执行,完成程序的功能。
字节码指令按功能可以分成九大类:
- 加载与存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用与返回指令
- 同步指令
- 异常处理指令
这九大类听起来多,但有规律可循。我们挑几个重点看看:
- 加载与存储指令:负责局部变量表和操作数栈的数据交互。操作数栈简单说,就是 JVM 执行计算时的临时存储区。比如 iload,把整数压入操作数栈;前缀 i 表示整数,l 表示长整型,a 代表引用类型,像字符串或对象。
- 运算指令:最熟悉的加减乘除。比如 iadd 是整数加法,前面加前缀就支持不同类型计算。
- 类型转换指令:处理数据类型转换。比如 int2byte,把整数转成字节。
- 对象创建与访问指令:常见的有 new 创建对象,getfield 和 putfield 读写字段。
- 控制转移指令:条件分支,像 ifeq 判断相等,还有 goto 这样的无条件跳转。
这九类指令加起来有 200 多个,大致了解规律和作用即可。比如,前缀 i、l、f 表示数据类型,很好记;每类指令的功能也很直观,像运算就是计算,同步就是多线程的 synchronized。
字节码指令是 JVM 的核心基础。它告诉我们字节码能做什么。字节码本身包含八大组成部分,最重要的是常量池;而指令这块,就是这九大类,覆盖了从计算到对象管理的各种操作。(指令)
到这儿,咱们对字节码指令的分布应该有个清晰印象了。后面有了操作数栈和内存区域,咱们就能更深切的见到 JVM 是怎样运作的了。