老程序员分享:Java虚拟机详解(九)

简介: 老程序员分享:Java虚拟机详解(九)

目录


1、Java虚拟机的两个特性①、语言无关性②、平台无关性2、class 字节码文件介绍①、字节码文件②、javap 命令3、无符号数和表4、魔数5、Class 文件的版本号6、常量池①、常量池容量计数值②、常量池内容7、访问标志8、类索引、父类索引和接口索引集合9、字段表集合10、方法表集合11、属性表集合


  我们知道计算机是由晶体管、电路板等组装而成的电子设备,而这些电子设备其实只能识别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 位字节进行存储。


  比如,对于如下这段代码:


1 /*


2 Create by YSOcean


3 */


4 public class ClassTest {


5 private static int i = 0;


6


7 public static void main(String【】 args) {


8 System.out.println(i);


9 }


10 }


  我们将生成的class 文件,通过十六进制编辑器打开(在IDEA中,可以下载HexView插件,安装完成后,选择这个class文件,右键 HexView)


  打开后的文件如下:(下面的介绍也都是以这张图为例)


  下面我们会介绍这些十六进制分别代表什么意思。


②、javap 命令


  另外,为了更好的查看 Class 文件字节码结构,JDK 还为我们提供了一个命令行工具 javap。使用语法如下:


javap


  通过 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文件结构中与其它项目关联最多的数据类型。


①、常量池容量计数值


  因//代码效果参考:http://www.lyjsj.net.cn/wx/art_23977.html

为常量池中常量的数量是不固定的,所以在常量池的入口要放置一项 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)


    字段的//代码效果参考:http://www.lyjsj.net.cn/wz/art_23975.html

名称和描述符(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 类型的常量来表示,其属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。


参考文档:


作者:IT可乐


出处:


本文版权归作者所有,欢迎转载,但未经作者同意不能转载,否则保留追究法律责任的权利。

相关文章
|
11天前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
34 9
|
3月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
137 4
|
3月前
|
算法 Java 程序员
在Java的编程世界里,多态不仅仅是一种代码层面的技术,它是思想的碰撞,是程序员对现实世界复杂性的抽象映射,是对软件设计哲学的深刻领悟。
在Java的编程世界里,多态不仅仅是一种代码层面的技术,它是思想的碰撞,是程序员对现实世界复杂性的抽象映射,是对软件设计哲学的深刻领悟。
64 9
|
3月前
|
Java 程序员
Java数据类型:为什么程序员都爱它?
Java数据类型:为什么程序员都爱它?
50 1
|
8天前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
13天前
|
IDE Java 程序员
C++ 程序员的 Java 指南
一个 C++ 程序员自己总结的 Java 学习中应该注意的点。
18 5
|
28天前
|
Java 大数据 程序员
我的程序员之路:自学Java篇
我的程序员之路:自学Java篇
|
4月前
|
Java 程序员 C++
大牛程序员用Java手写JVM:刚好够运行 HelloWorld
大牛程序员用Java手写JVM:刚好够运行 HelloWorld
|
3月前
|
安全 Java 程序员
阿里开发手册 嵩山版-编程规约 (四)OOP规约-Java程序员必看知识点!!!
《阿里开发手册 嵩山版》的OOP规约部分强调了面向对象编程的最佳实践,包括正确使用静态方法、覆写方法的注解、可变参数的使用、接口的稳定性、equals和compareTo方法的使用、BigDecimal的正确比较、包装类与基本数据类型选择、POJO类的属性和方法设计等,以提升代码的质量和维护性。
|
3月前
|
设计模式 前端开发 Java
Spring,作为Java程序员的你能想到什么呢?
该文章主要介绍了Spring框架对于Java程序员的意义,包括Spring框架的一些核心能力和为什么它是如此重要。