Tianyi's Blog Tianyi's Blog
首页
  • 计算机网络
  • 操作系统
  • 计算机科学
  • Nginx
  • Vue框架
  • 环境配置
  • Java
  • JVM
  • Spring框架
  • Redis
  • MySQL
  • RabbitMQ
  • Kafka
  • Mirror Sites
  • Dev Tools
  • Docker
  • Jenkins
  • Scripts
  • Windows
  • 科学上网
  • 旅行
  • 网站日记
  • 软件
  • 电子产品
  • 杂野
  • 分类
  • 友情链接
GitHub (opens new window)

Tianyi

一直向前,永不停止
首页
  • 计算机网络
  • 操作系统
  • 计算机科学
  • Nginx
  • Vue框架
  • 环境配置
  • Java
  • JVM
  • Spring框架
  • Redis
  • MySQL
  • RabbitMQ
  • Kafka
  • Mirror Sites
  • Dev Tools
  • Docker
  • Jenkins
  • Scripts
  • Windows
  • 科学上网
  • 旅行
  • 网站日记
  • 软件
  • 电子产品
  • 杂野
  • 分类
  • 友情链接
GitHub (opens new window)
  • Java

  • Golang

  • JVM的奇妙世界

    • 虚拟机1-宏观视角
    • 虚拟机2-字节码
      • 字节码的组成
        • 先聊魔数
        • 再说文件版本
        • 讲个常见问题
        • 常量池
        • 1. 字面量
        • 2. 符号引用
        • 3. 常量池数据类型表
        • 字面量示例
        • 符号引用示例
        • 总结
      • 类索引与访问标志
        • 1. 类、父类、接口索引
        • 小例子
        • 2. 访问标志
        • 小例子
        • 总结
      • 字段表、方法表与属性表
        • 一、字节码的整体结构
        • 二、字段表(Fields Table)
        • 三、方法表(Methods Table)
        • 四、属性表(Attributes Table)
      • 字节码指令
        • 字节码指令的格式和特点
        • 字节码指令的实际应用
        • 字节码指令长什么样?
        • 总结
      • 字节码指令_与他的200多号兄弟
        • 字节码指令的分类
    • 虚拟机3-输入层
    • 虚拟机4-运行时数据区
    • 虚拟机5-垃圾收集器
    • 虚拟机6-调优
    • JVM调优
  • Spring

  • Spring增强封装

  • Redis

  • MySQL

  • RabbitMQ

  • Kafka

  • 分享

  • 后端
  • JVM的奇妙世界
tianyi
2024-03-29
目录

虚拟机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。

# 总结

常量池的核心内容就是这三类:

  1. 字面量:字符串和 final 常量,存具体的值。
  2. 符号引用:类、方法、字段的描述,存“指针”信息,引用到实际的位置。
  3. 数据类型表:类型简写,比如 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字节码的组成。一个字节码文件包含八个部分:

  1. 魔术(Magic Number):前4个字节,通常是“CAFEBABE”,标识这是字节码文件。
  2. 文件版本:记录编译时的JVM版本。
  3. 常量池:存储所有常量数据,比如字符串、类名等,是字节码的核心。
  4. 访问标志:定义类或方法的访问权限,比如public、static。
  5. 类、父类和接口索引集合:指明类的继承关系和实现的接口。
  6. 字段表集合:描述类中的字段。
  7. 方法表集合:描述类中的方法。
  8. 属性表:提供辅助信息。

在下面的内容里我们聚焦后三项:字段表、方法表和属性表。他们的区别其实就是一个是类的,一个是方法的,一个是共享的,一个是私有的。

# 二、字段表(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调用方法)。

# 字节码指令的实际应用

来看两个例子:

  1. invokevirtual #8
    • 含义:调用常量池中第8号索引的方法。
    • 假设常量池#8是StringBuilder.append,JVM执行这条指令时,就会调用StringBuilder对象的append方法,完成字符串追加。
  2. 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>)
1
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 收到这些指令,就按顺序执行,完成程序的功能。

字节码指令按功能可以分成九大类:

  1. 加载与存储指令
  2. 运算指令
  3. 类型转换指令
  4. 对象创建与访问指令
  5. 操作数栈管理指令
  6. 控制转移指令
  7. 方法调用与返回指令
  8. 同步指令
  9. 异常处理指令

这九大类听起来多,但有规律可循。我们挑几个重点看看:

  • 加载与存储指令:负责局部变量表和操作数栈的数据交互。操作数栈简单说,就是 JVM 执行计算时的临时存储区。比如 iload,把整数压入操作数栈;前缀 i 表示整数,l 表示长整型,a 代表引用类型,像字符串或对象。
  • 运算指令:最熟悉的加减乘除。比如 iadd 是整数加法,前面加前缀就支持不同类型计算。
  • 类型转换指令:处理数据类型转换。比如 int2byte,把整数转成字节。
  • 对象创建与访问指令:常见的有 new 创建对象,getfield 和 putfield 读写字段。
  • 控制转移指令:条件分支,像 ifeq 判断相等,还有 goto 这样的无条件跳转。

这九类指令加起来有 200 多个,大致了解规律和作用即可。比如,前缀 i、l、f 表示数据类型,很好记;每类指令的功能也很直观,像运算就是计算,同步就是多线程的 synchronized。

字节码指令是 JVM 的核心基础。它告诉我们字节码能做什么。字节码本身包含八大组成部分,最重要的是常量池;而指令这块,就是这九大类,覆盖了从计算到对象管理的各种操作。(指令)

到这儿,咱们对字节码指令的分布应该有个清晰印象了。后面有了操作数栈和内存区域,咱们就能更深切的见到 JVM 是怎样运作的了。

完善页面 (opens new window)
虚拟机1-宏观视角
虚拟机3-输入层

← 虚拟机1-宏观视角 虚拟机3-输入层→

最近更新
01
MySQL 优化思路
08-30
02
JDK
02-23
03
BadTasteCode && 优化
09-11
更多文章>
Theme by Vdoing | Copyright © 2021-2025 Tandy | 粤ICP备2023113440号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式