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
目录
字节码的组成
先聊魔数
再说文件版本
讲个常见问题
常量池
1. 字面量
2. 符号引用
3. 常量池数据类型表
字面量示例
符号引用示例
总结
类索引与访问标志
1. 类、父类、接口索引
小例子
2. 访问标志
小例子
总结
字段表、方法表与属性表
一、字节码的整体结构
二、字段表(Fields Table)
三、方法表(Methods Table)
四、属性表(Attributes Table)
字节码指令
字节码指令的格式和特点
字节码指令的实际应用
字节码指令长什么样?
总结
字节码指令_与他的200多号兄弟
字节码指令的分类

虚拟机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
JDK
02-23
02
BadTasteCode && 优化
09-11
03
Gradle 实践操作指南及最佳实践
09-11
更多文章>
Theme by Vdoing | Copyright © 2021-2025 Tandy | 粤ICP备2023113440号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式