Java虚拟机详解(九)------类文件结构
我们知道计算机是由晶体管、电路板等组装而成的电子设备,而这些电子设备其实只能识别0与1的信号。
那么问题来了,我们在操作系统上编写的Java代码(由字母、数字等各种符号组成),打包后部署到服务器上,是如何被计算机所识别并运行的呢?另外,操作系统有很多种,包括Windows系统,Linux系统,Mac OS系统等,而我们同样的Java代码,却可以不做任何处理在不同的系统上正常运行,这又是为啥呢?
带着这些疑问,你将会在下面的介绍中得到答案!!!
1、Java虚拟机的两个特性
在此系列博客第一篇文章中,我们介绍到Java虚拟机的两个特性。
①、语言无关性
对于Java语言,我们通过编辑器编写的Java代码,后缀一般是.java。通过javac编译器编译后,会变成.class结尾的字节码文件,只有编译后的.class文件,才能在Java虚拟机上运行。(解压部署在服务器上的jar包,全是编译后的class文件)
再比如对于 JRuby 语言,通过编辑器编写的代码后缀是.rb。通过jrubyc 编译器编译后,也会变成 .class 结尾的字节码文件,然后也能在Java虚拟机上运行。
在比如已正式成为Android官方支持开发语言的Kotlin。也可以编译成.class字节码文件,然后在虚拟机上运行。
我们可以用下面这幅图来表示:
也就是说,不管你是什么语言,只要能通过某种手段生成合乎规范的.class字节码文件,其实就可以在Java虚拟机上运行,这就是语言无关性。
②、平台无关性
Write once, run everywhere(一次编写,到处运行)这是Java语言诞生之处就宣传的一个口号。Java语言之所以能够跨平台运行,其实就是因为Java虚拟机对各个平台的适配,在不同的系统下安装不同的Java虚拟机,我们程序当然能够在不同的系统上运行。
对于文章开头提出的问题,同样的程序能够在不同的系统上正常运行的原因,就是因为我们在不同的系统上安装了不同的Java虚拟机。
2、class 字节码文件介绍
搞清楚了Java代码的跨平台原理,我们接着来介绍为什么编写的Java代码能够被计算机所识别。
①、字节码文件
这其实是上面所说的语言无关性这个特性重要文件——class字节码文件的功劳。
Java所有的指令大概有 200 个左右,一个字节(8位)可以存储 256 种不同的信息,我们将一个这样的字节称为字节码(ByteCode)。
而 class 文件便是一组以 8 位字节为基础单位流的二进制流,各个数据项目严格按照顺序紧凑地排列在 class 文件之中,中间没有添加任何分隔符,所以整个class 文件中存储的内容几乎都是程序运行的必要数据,没有任何冗余。当遇到需要占用 8 位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。
比如,对于如下这段代码:
/**
* Create by YSOcean
*/
public class ClassTest {
private static int i = 0; public static void main(String[] args) {
System.out.println(i);
}
}
我们将生成的class 文件,通过十六进制编辑器打开(在IDEA中,可以下载HexView插件,安装完成后,选择这个class文件,右键 HexView)
打开后的文件如下:(下面的介绍也都是以这张图为例)
下面我们会介绍这些十六进制分别代表什么意思。
②、javap 命令
另外,为了更好的查看 Class 文件字节码结构,JDK 还为我们提供了一个命令行工具 javap。使用语法如下:
javap <options> <classes>
通过 javap -help 命令,可以查看相关参数作用:
我们将 ClassTest.class 文件,通过 javap -v ClassTest.class 命令,执行后如下:
这些内容下面也会详细介绍。
3、无符号数和表
在介绍这些十六进制之前,我们先介绍 Class 文件的数据类型。
Class 文件采用一种类似于 C 语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表。
①、无符号数
这是一种基本数据类型,以 u1,u2,u4,u8 来分别代表 1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或按照 UTF-8 编码构成的字符串值。
②、表
表是由多个无符号数或其它表作为数据项所构成的复合数据类型,所有表都习惯行的以“_info”结尾。表用于描述有层次关系的复合结构数据。
整个 Class 文件本质上就是一张表,结构如下:
PS:需要说明的是,由于 Class 文件结构没有任何分隔符,所以无论是每个数据项的的顺序还是数量,都是严格限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都是不允许改变的。
下面,我们就来分别介绍这些数据项代表什么含义。
4、魔数
每个 class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是:标识该文件是一个Java类文件。如果没有识别到该标志,则说明该文件不是Java类文件或者文件已受损。
由上图,我们可以看到前 4 个字节是 cafe babe。这是 Gosling 定义的一个魔法数,意思是 Coffee Baby。
其实很多文件存储标准中都使用魔数进行身份识别,比如图片gif或者jpeg,使用魔数而不是使用扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以任意的改动。
5、Class 文件的版本号
紧随魔数的 4 个字节存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。
Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.0~JDK1.1使用了45.0~45.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。
上图第5、6、7、8个字节为 00 00 00 34。其十进制值为 52,是JDK8的内部版本号。
6、常量池
紧随主版本号的是常量池入口,是class文件中第一个出现的表类型数据项目,也是占用Class文件空间最大的项目之一,更是Class文件结构中与其它项目关联最多的数据类型。
①、常量池容量计数值
因为常量池中常量的数量是不固定的,所以在常量池的入口要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
PS:注意,常量池容量计数值是从 1 开始的,而不是从 0 开始。将 0 空出来,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。
Class 文件结构中,只有常量池的容量是从 1 开始的,其它的集合类型,都是从 0 开始的。
看上图的十六进制文件,常量池容量计数值为:0x0025,即十进制 37。这就表示常量池中有 36 项常量,索引值分别为 1~36(通过上面javap命令生成字节码文件可以很明显看出来有36个)
②、常量池内容
常量池主要存放两大类常量:
1、字面量(Literal):字面量比较接近于 Java 语言层面的常量概念,比如 文本字符串、被声明为 final 的常量值等。
2、符号引用(Symbolic References):符号引用属于编译原理方面的概念,包括下面三类常量:
类和接口的权限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符。
需要说明的是,Java代码在进行javac 编译的时候,并不像 C 和 C++ 那样有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。
也就是说,在 Class 文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。关于类的创建和动态连接的内容,下篇博客会详细介绍。
常量池中的每一项内容都是一个表,在JDK1.8中共有 14 种结构各不相同的表结构数据,每个表结构第一位是一个 u1 类型的标志位(tag,取值为1 到 18,缺少标志为 2、13、14、17 的数据类型)。代表当前这个常量属于哪种常量类型。
14 种常量类型所代表的具体含义如下:
接着看十六进制文件,紧跟常量池数量的十六进制是0x0a,这是一个标志位,0x0a的十进制数是10,查看常量池的项目表接口,表示的类型是 CONSTANT_Methodref_info。
也就是说,接下来的u2类型0x0006,其十进制值为6,紧跟后面的u2类型十六进制为0x0017,其十进制值为23,这都是两个索引值,分别指向第索引值为6的常量和索引值为23的常量。
整个十六进制字节码就不一一进行推导了,下面是各个数据类型的结构:
7、访问标志
常量池结束后的两个字节表示访问标志(access_flags),这个标识用于识别一些类或接口层次的访问信息。
包括:这个 Class 是类还是接口;是否定义为 public 类型,是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。
具体的标志位及标志含义如下:
上表定义了 8 个标志位,但是我们说访问标志是一个 u2 类型,一共有 32 个标志位可以使用,没有定义的标志位一律为 0 。
8、类索引、父类索引和接口索引集合
类索引、父类索引和接口索引按顺序排列在访问标志之后。
类索引:用于确定这个类的全限类名 ,是一个 u2 类型的数据。
父类索引:用于确定这个类的父类全限类名,也是一个 u2 类型的数据。因为Java是单继承的,除了 java.lang.Object 类以外,所有的类都有父类。所以,除了Object 类以外,所有Java类的父类索引都不为0.
接口索引:用于描述这个类实现了哪些接口,是一组 u2 类型的数据集合,第一项为 u2 类型的接口计数器,表示实现接口的个数。如果没有实现任何接口,则为0。
9、字段表集合
字段表(field_info):描述接口或类中声明的变量。(不包括方法内部声明的变量)
描述的信息包括:
①、字段的作用域(public,protected,private修饰)
②、是类级变量还是实例级变量(static修饰)
③、是否可变(final修饰)
④、并发可见性(volatile修饰,是否强制从主从读写)
⑤、是否可序列化(transient修饰)
⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)
⑦、字段名称
前面5个修饰符,都是布尔值,用标志位来表示;后面两个字段名称和类型,是无法固定的,只能引用常量池中的常量来表示。
access_flags 是一个 u2 类型,表示各种修饰符。
10、方法表集合
Class 文件存储格式中对方法的描述和字段的描述基本上是一致的。也是依次包括:
访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合数量(attributes_count)、属性表集合(attributes)
方法访问标志如下(access_flags):
11、属性表集合
在前面介绍的字段表集合、方法表集合中都包括了属性表集合(attributes),其实就是引用的这里。
根据《Java虚拟机规范第二版》中,预定义了 9 项虚拟机实现应当能够识别的属性。
对于每一个属性,它的名称要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,其属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。
参考文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
Java虚拟机详解(九)------类文件结构的更多相关文章
- Java虚拟机详解----JVM常见问题总结
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- Java 虚拟机详解
深入理解JVM 1 Java技术与Java虚拟机 说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言.Java类文件格式.Java虚 ...
- Java虚拟机详解(十)------类加载过程
在上一篇文章中,我们详细的介绍了Java类文件结构,那么这些Class文件是如何被加载到内存,由虚拟机来直接使用的呢?这就是本篇博客将要介绍的——类加载过程. 1.类的生命周期 类从被加载到虚拟机内存 ...
- Java虚拟机详解(二)------运行时内存结构
首先通过一张图了解 Java程序的执行流程: 我们编写好的Java源代码程序,通过Java编译器javac编译成Java虚拟机识别的class文件(字节码文件),然后由 JVM 中的类加载器加载编译生 ...
- Java虚拟机详解04----GC算法和种类【重要】
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- Java虚拟机详解04----GC算法和种类
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- Java虚拟机详解02----JVM内存结构
主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启动时,是由java命令/javaw命令来启动的. 二.JVM基本结构: JVM基本结构图: ...
- java虚拟机详解
注: 此篇文章可以算是读<深入理解Java虚拟机:JVM高级特性与最佳实践>一书后的笔记总结加上我个人的心得看法. 整体总结顺序沿用了书中顺序,但多处章节用自己的话或直白或扩展的进行了重新 ...
- Java虚拟机详解----JVM内存结构
http://www.cnblogs.com/smyhvae/p/4748392.htm 主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启 ...
随机推荐
- Markdown的基本使用指南
目录 1.标题 2.列表 2.1无序列表 2.1有序列表 3.引用 4.图片和链接 5.粗体和斜体 6.分割线 7.代码框和代码块 8.列表 9.可选框 10.添加目录 Markdown 是一种用来写 ...
- 工厂模式在mvc模型中的应用
在web开发中我们常用mvc模式进行web应用的开发 当应用进入service 层的时候我们根据不同的业务多逻辑进行处理 当有数据进入controller的时候 public class Virtua ...
- wpf 依赖强制回调
依赖属性的强制转换加回调,后期自己再改 using System; using System.Collections.Generic; using System.Diagnostics; using ...
- The usage of Markdown---杂谈:缩进/换行/分割线/注释/文字颜色
目录 1. 序言 2. 缩进 3. 换行 4. 分割线 5. 注释 6. 改变文字大小和颜色 更新时间:2019.09.14 1. 序言 今天一口气更新了接近10篇博客(虽然只是将我之前基本码好的 ...
- Mybatis总结之如何自动生成数据库表结构
一般情况下,用Mybatis的时候是先设计表结构再进行实体类以及映射文件编写的,特别是用代码生成器的时候. 但有时候不想用代码生成器,也不想定义表结构,那怎么办? 这个时候就会想到Hibernate, ...
- 禁止浏览器缓存- make sure web page is not cached
如何禁止浏览器缓存,网上搜到的解决方法都测试无效. 基本上全都是 Cache-Control: no-cache Pragma: no-cache Expires: 0 Google了一下,找到了解决 ...
- Java基础(三)对象与类
1.类的概念:类是构造对象的模板或蓝图.由类构造对象的过程称为创建类的实例. 2.封装的概念:封装(有时称为数据隐藏)是与对象有关的一个重要概念.对象中的数据称为实例域,操纵数据的过程称为方法.对于每 ...
- Linux下安装db2V9.7
vi /etc/hosts(127.0.0.1 localhost192.168.1.53 linux-wmv8) vi /etc/services db2inst1 50000/tcp(加在最后) ...
- springcloud之Feign、ribbon设置超时时间和重试机制的总结
一 超时时间配置 如果在一个微服务当中对同一个接口同时配置了Hystrix与ribbon两个超时时间,则在接口调用的时候,两个计时器会同时读秒. 比如,访问一个接口需要2秒,你的ribbon配置的超时 ...
- 务必收藏备用:.net core中通过Json或直接获取图形验证码(数字验证码、字母验证码、混合验证码),有源代码全实战demo(开源代码.net core3.0)
很多人写的博客大家看了会一知半解,不知道怎么用,应该引用什么类库或者代码不全,这样很多小白很是头疼,尤其是尝新技术更是如此.我们这边不止告诉你步骤,而且还提供开源demo.随着时间的推移,我们的dem ...