很多时候,我们都是从代码层面去学习如何编程,却很少去看看一个个 Java 代码背后到底是什么。今天就让我们从一个最简单的 Hello World 开始看一看 Java 的类文件结构。

在开始之前,我们先写一个最简单的入门 Hello World。

public class Demo{
public static void main(String args[]){
System.out.println("Hello World.");
}
}

接着在命令行运行javac Demo.java命令编译这个类,这时会生成一个 Demo.class 文件。

接着我们用纯文本编辑器打开生成的 Demo.class 文件。

cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 4465 6d6f 2e6a
6176 610c 0007 0008 0700 170c 0018 0019
0100 0b48 656c 6c6f 2057 6f72 6c64 0700
1a0c 001b 001c 0100 0444 656d 6f01 0010
6a61 7661 2f6c 616e 672f 4f62 6a65 6374
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0013 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d01 0007 7072 696e 746c
6e01 0015 284c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5600 2100 0500 0600
0000 0000 0200 0100 0700 0800 0100 0900
0000 1d00 0100 0100 0000 052a b700 01b1
0000 0001 000a 0000 0006 0001 0000 0001
0009 000b 000c 0001 0009 0000 0025 0002
0001 0000 0009 b200 0212 03b6 0004 b100
0000 0100 0a00 0000 0a00 0200 0000 0300
0800 0400 0100 0d00 0000 0200 0e

可以看到我们简单的 5 行代码到最后就浓缩成了上面那一长串数字字母组成的十六进制符号。而当我们运行该 Java 类时,控制台能准确地输出「Hello World」,所以们可以断定这一长串的符号必定遵守着某种规则,而这个规则其实就是:Java虚拟机规范

Java虚拟机规范

Java 虚拟机规范中规定了 Java 虚拟机结构、Class 类文件结构、字节码指令等内容,其中对于软件开发人员来说,类文件结构是有必要了解的一个内容。

Java 虚拟机的类文件结构是一组以 8 位字节为基础的二进制流,各数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全都是程序需要的数据,没有空隙存在。

如果你对 Java 虚拟机规范有兴趣,可以参考 Github 上的《Java 虚拟机规范》

Java 虚拟机

说完了 Java 虚拟机规范,就需要了解一下 Java 虚拟机这个概念。

其实 Java 虚拟机就是一个虚拟的计算机。与真实的计算机一样,Java 虚拟机有自己完善的硬件体系,如处理器、堆栈、寄存器,还有相应的指令集系统。虚拟机与我们的电脑唯一的区别是:虚拟机的处理器、内存堆栈是用软件虚拟出来的,而我们电脑的处理器和内存则是真真实实的。

虽然名字是叫 Java 虚拟机,但 Java 虚拟机与 Java 语言没有直接关系,它只按照 Java 虚拟机规范去读取 Class 文件,并按照规定去解析、执行字节码指令,仅此而已。

如果你够牛逼,你完全可以写一个编译器,将 C 语言代码编译成符合 Java 虚拟机规范的字节码文件,那么 Java 虚拟机也是可以执行的。

准确地说,Java 虚拟机与字节码文件(Class文件)绑定。

Java类文件结构

Java 虚拟机规范中定义了许多规范,其中有一部分定义了字节码的结构和规范。Java 虚拟机规范定义了两种数据类型来表示 Class 文件格式,分别是:无符号数和表。

无符号数属于最基本的数据类型,以 u1、u2、u4、u8 六七分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 个字节表示该文件的魔数,第二行的 u2 表示该 Class 文件第 5-6 个字节表示该 JDK 的次版本号。

是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,表用于描述有层次关系的复合结构的数据。例如下表第 5 行表示其实一个类型为 cp_info 的表(常量池),这里面存储了该类的所有常量。

整个 Class 文件本质上就是一张表,它由表下表所示的数据项构成。

上面的表其实可以划分为以下七个部分,这七个部分组成了一个完整的 Class 字节码文件:

  • 魔数与Class文件版本
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引
  • 字段表集合
  • 方法表集合
  • 属性表集合

接下来我们用上面「Hello World」的字节码文件为例子,一步步分析这七部分内容。

魔数与Class文件版本

Class 文件的第 1 - 4 个字节代表了该文件的魔数(Magic Number)。它唯一的作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件,其值固定是:0xCAFEBABE(咖啡宝贝)。如果一个 Class 文件的魔数不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件。

Class 文件的第 5 - 6 个字节代表了 Class 文件的次版本号(Minor Version),即编译该 Class 文件的 JDK 次版本号。

Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号(Major Version),即编译该 Class 文件的 JDK 主版本号。

高版本的 JDK 能向下兼容以前笨笨的 Class 文件,但不能运行新版本的 Class 文件。例如一个 Class 文件是使用 JDK 1.5 编译的,那么我们可以用 JDK 1.7 虚拟机运行它,但不能用 JDK 1.4 虚拟机运行它。下表列出了各个版本 JDK 的十六进制版本号信息:

我们看看之前的 Demo 文件的 Class 文件,其前 8 个字节分别是:cafe babe 0000 0034。那么我们可以知道,这个 Class 文件是由 JDK1.8 编译的。

文章首发于【博客园-陈树义】,点击跳转到原文《从 HelloWorld 看 Java 字节码文件结构》

常量池

Class 文件的第 9 - 10 个字节用于表示常量池常量的个数(constant_pool_count),那么紧跟着就有 constant_pool_count - 1 个常量。我们Class 文件第 9 - 10 个字节为 001d,表示有 28 个常量。

每个常量池的常量都用一个类型为 cp_info 的表表示,该表有 14 个值,分别是:

第 1 个常量。紧接着 001d 的后一个字节为 0A,表示该常量为方法引用类型(CONSTANT_MethodHandle_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示类信息,这里是 0006 表示指向常量池第 6 个常量所表示的信息。该常量项的第 4 - 5 个字节表示名称及类描述符,这里值为 000f 表示指向常量池第 10 个常量所表示的信息。

第 2 个常量。紧接着 000f 的后一个字节为 09,表示该常量为字段引用类型(CONSTANT_Fieldref_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示类信息,这里是 0010 表示指向常量池第 16 个常量所表示的信息。该常量项的第 4 - 5 个字节表示名称及类描述符,这里值为 0011 表示指向常量池第 17 个常量所表示的信息。

第 3 个常量。紧接着 0011 的后一个字节为 08,表示该常量为字符串引用类型(CONSTANT_String_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示指向字符串字面量的索引,这里是 0012 表示指向常量池的第 18 个常量。

第 4 个常量。紧接着 0012 的后一个字节为 0A,表示该常量为方法引用类型(CONSTANT_MethodHandle_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示类信息,这里是 0013 表示指向常量池第 19 个常量所表示的信息。该常量项的第 4 - 5 个字节表示名称及类描述符,这里值为 0014 表示指向常量池第 20 个常量所表示的信息。

第 5 个常量,是类信息类型常量,其指向了常量池第 21 个常量。

第 6 个常量,是类信息类型常量,其指向了常量池第 22 个常量。

第 7 个常量。这里表示 tag 的值是 01,表示该常量为一个字符串(CONSTANT_Utf8_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示该字符串的长度,这里是 0006 表示该字符串长度为 6 个字节。这里紧接着 01 的六个字节为 3C 69 6E 69 74 3E。在 Class 文件中,字符串是使用 ASCII 码进行编码的,我们将这些十六进制字符转换成对应的 ASCII 码之后,其值为:<init>

第 8 个常量,是一个字符串常量,转换之后是:()V

第 9 个常量,是一个字符串常量,转换之后是:Code

第 10 个常量,是一个字符串常量,转换之后是:LineNumberTable

第 11 个常量,是一个字符串常量,转换之后是:main

第 12 个常量,是一个字符串常量,转换之后是:([Ljava/lang/String;)V

第 13 个常量,是一个字符串常量,转换之后是:SourceFile

第 14 个常量,是一个字符串常量,转换之后是:Demo.java

第 15 个常量。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 0007 表示指向常量池第 7 个常量所表示的信息。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 0008 表示指向常量池第 8 个常量所表示的信息。根据我们之前的分析,可以知道第 15 个常量表示的信息其实是:"<init>":()V

第 16 个常量。这里表示 tag 的值是 07,表示该常量为类信息类型(CONSTANT_Class_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示全限定名常量项的索引,这里是 0017 表示指向常量池第 23 个常量所表示的信息。

第 17 个常量。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 0018 表示指向常量池第 24 个常量所表示的信息。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 0019 表示指向常量池第 25 个常量所表示的信息。根据我们之前的分析,可以知道第 17 个常量表示的信息其实是:out:Ljava/io/PrintStream;

第 18 个常量,是一个字符串常量,转换之后是:Hello World

第 19 个常量。这里表示 tag 的值是 07,表示该常量为类信息类型(CONSTANT_Class_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示全限定名常量项的索引,这里是 001A 表示指向常量池第 26 个常量所表示的信息。

第 20 个常量。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 001B 表示指向常量池第 27 个常量所表示的信息。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 001C 表示指向常量池第 28 个常量所表示的信息。

第 21 个常量,是一个字符串常量,转换之后是:Demo

第 22 个常量,是一个字符串常量,转换之后是:java/lang/Object

第 23 个常量,是一个字符串常量,转换之后是:java/lang/System

第 24 个常量,是一个字符串常量,转换之后是:out

第 25 个常量,是一个字符串常量,转换之后是:Ljava/io/PrintStream;

第 26 个常量,是一个字符串常量,转换之后是:java/io/PrintStream

第 27 个常量,是一个字符串常量,转换之后是:println

第 28 个常量,是一个字符串常量,转换之后是:(Ljava/lang/String;)V

到这里,我们常量池里 28 个常量已经全部解析完了。我们通过手动分析,了解了常量池的构成,但很多时候我们可以借助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息。

当我们运行javap -verbose Demo.class时,控制台会打印出该 Class 文件的构成信息,其中就包括了常量池的信息。

将利用 javap 打印出的结果,与我们手动分析的结果对比一下,你会发现结果是一致的。

访问标志

在常量池结束之后,紧接着的两个字节代表访问标记(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型等。具体的标志位以及标志的含义见下表。

在这里这两个字节是 00 21,通过查看我们并没有发现有标志值是 00 21 的标志名称。这是因为这里的访问标志可能是由多个标志名称组成的,所以字节码文件中的标志值其实是多个值进行或运算的结果。

通过查阅上述表格,我们可以知道,00 21 由 00 01 和 00 20 进行或运算得来。也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义。

类索引、父类索引、接口索引

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。

类索引。类索引用于确定这个类的全限定名,它用一个 u2 类型的数据表示。这里的类索引是 00 05 表示其指向了常量池中第 5 个常量,通过我们之前的分析,我们知道第 5 个常量其最终的信息是 Demo 类。

父类索引。父类索引用于确定这个类的父类的全限定名,父类索引用一个u2类型的数据表示。这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量,通过我们之前的分析,我们知道第 6 个常量其最终的信息是 Object 类。因为其并没有继承任何类,所以 Demo 类的父类就是默认的 Object 类。

接口索引。接口索引集合就用来描述哪个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口第一项是 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,而在接口计数器后则紧跟着所有的接口信息。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

这里 Demo 类的字节码文件中,因为并没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没有实现任何接口。因此后面的接口索引表为空。

字段表集合

字段表集合用于描述接口或者类中声明的变量。这里说的字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。

在类接口集合后的2个字节是一个字段计数器,表示总有有几个属性字段。在字段计数器后,才是具体的属性数据。字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:

因为我们并没有声明任何的类成员变量或类变量,所以在 Demo 的字节码文件中,字段计数器为 00 00,表示没有属性字段。

方法表集合

在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法。在字段计数器后,才是具体的方法数据。方法表中的每个方法都用一个 method_info 表示,其数据结构如下:

Demo 类的字节码文件中,方法计数器的值为 00 02,表示一共有 2 个方法。

第 1 个方法。方法计数器后 2 个字节表示方法访问标识,这里是 00 01,表示其实 ACC_PUBLIC 标识,即该方法访问表示为 public。紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了<init>。紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V。紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法一共有 1 个属性。紧接着的一连串就是属性表的内容。

到这里我们通过对 Hello World 的解析,从而对 Java 类文件结构有了一个全面的认识。进一步还简单了解了 Java 虚拟机以及 Java 虚拟机规范。希望读完这篇文章,大家能对 Java 类文件结构有一个深入的认识。

如果读完觉得有收获,可以点赞评论,让我写出更多的好文章。

文章首发于【博客园-陈树义】,点击跳转到原文《从 HelloWorld 看 Java 字节码文件结构》

从 HelloWorld 看 Java 字节码文件结构的更多相关文章

  1. Java字节码文件结构剖析

    今天起开启JVM的新的知识学习篇章----Java的字节码,那学习Java字节码有啥用呢?我们知道Java是跨平台的一门语言,编写一次到处运行,而支撑着这个特性的根基为两点:JVM和.class字节码 ...

  2. Java字节码文件结构---概述

    一.Class文件的结构概述: 是一连串的字节流(以自节为基本单位划分),里面包含的数据项按照固定的次序依次排列组成Class文件,文件内部不含分割符 当数据项的长度大于1B时候,按照高位在前的方式存 ...

  3. Java字节码深度剖析

    Java字节码文件查看 我们有一个类Test01,具体内容如下: package bytecode; public class Test01 { private int i = 0; public i ...

  4. 轻松看懂Java字节码

    java字节码 计算机只认识0和1.这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行.所以,我们所编写的程序在不同的平台上运行前都要经过重新编译才能被执行. 而Java刚诞生 ...

  5. Java字节码操纵框架ASM小试

    本文主要内容: ASM是什么 JVM指令 Java字节码文件 ASM编程模型 ASM示例 参考资料汇总 JVM详细指令 ASM是什么 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既 ...

  6. JVM基础系列第5讲:字节码文件结构

    温馨提示:此篇文章长达两万字,图片50多张,内容非常多,建议收藏后再看. 前面我们说到 Java 虚拟机使用字节码实现了跨平台的愿景,无论什么系统,我们都可以使用 Java 虚拟机解释执行字节码文件. ...

  7. JVM 内部原理(六)— Java 字节码基础之一

    JVM 内部原理(六)- Java 字节码基础之一 介绍 版本:Java SE 7 为什么需要了解 Java 字节码? 无论你是一名 Java 开发者.架构师.CxO 还是智能手机的普通用户,Java ...

  8. 【Java虚拟机1】Java字节码文件格式入门

    第一次学习看字节码文件,这个对工作没什么用,但是会提升内功. 首先介绍两个IDEA插件以及使用: BinEd:以16进制格式查看class文件 使用方法:右键class文件,点击Open as bin ...

  9. 用Eclipse插件Bytecode Outline来查看Java字节码

    在遇到一些小问题的时候我们经常会使用Javap反编译取得字节码来分析,虽然Javap能完成这个工作,但是有两个缺点,一方面操作麻烦,需要很多步骤,一方面没有文档注释,对新手来说看起字节码来比较麻烦. ...

随机推荐

  1. centos6下从源码安装setuptools和pip

    1. 下载setuptools及pip的源码包 setuptools与pip都是python的模块 setuptools源码包: https://pypi.python.org/pypi/setupt ...

  2. git 版本控制的简单应用

    一.通过 honebrew 安装git , 教程参考:http://brew.sh/index_zh-cn.html 也可对比参考:http://book.51cto.com/art/201107/2 ...

  3. Redis 实践3-操作

    string常用操作 set key1  aminglinux get key1   set key1  aming //一个key对应一个value,多次赋值,会覆盖前面的value setnx k ...

  4. 图论算法-Tarjan模板 【缩点;割顶;双连通分量】

    图论算法-Tarjan模板 [缩点:割顶:双连通分量] 为小伙伴们总结的Tarjan三大算法 Tarjan缩点(求强连通分量) int n; int low[100010],dfn[100010]; ...

  5. IDA学习笔记 函数调用约定

    stdcall和cdecl: stdcall和cdecl 压栈方向都是从右到左 区别在于c约定是调用方在函数返回后add esp,n指令清除堆栈中的参数,而stdcall在被调函数内使用ret n来清 ...

  6. Apollo阿波罗配置中心docker

    前言 在分布式系统中,要改个配置涉及到很多个系统,一个一个改效率低下,吃力不讨好.用配置中心可以解决这个问题.当然配置中心有不少,以下对比的表格是照搬Apollo Wiki的. 功能点 Apollo ...

  7. php进阶之路--转载

    之前有看过相关的文章,觉得还是这篇详细点,有具体的目标实现起来才更有动力 转载自:http://wen.52fhy.com/2016/2016-09-03-PHP-cheng-xu-yuan-xue- ...

  8. java中public private protected default的区别

    1.public:public表明该数据成员.成员函数是对所有用户开放的,所有用户都可以直接进行调用 2.private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直 ...

  9. Yii2自带的验证码背景颜色怎么调?

    看了下面的这张图片你就知道啦!

  10. Mysql利用存储过程插入400W条数据

    CREATE TABLE dept( /*部门表*/ deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, /*编号*/ dname VARCHAR(20) NO ...