网站首页 > java教程 正文
内容概要:
- javac编译器如何将java代码编译成字节码文件
- 字节码文件中都有些什么
前言:
java是一门基于虚拟机运行的语言,它创造了一套指令集用于在虚拟机(JVM)中运行。这是java之所以要先将java代码编译成字节码文件(.class文件)的原因, java一直宣传的“一次编译,到处运行”也是得益于此。
首先,希望大家能将java语言和JVM分开理解。在java语言诞生之时JVM也随之诞生,之后java语言越来越成熟,基于JVM虚拟机的运行方式也越来越被接受,事实上现在除了java还有很多语言基于JVM虚拟机运行,比如:Scala、Jython、JRuby、Groovy、kotlin等等。目前主流的java虚拟机是HotSpot虚拟机,无论是OracleJDK还是OpenJDK中它都是默认的虚拟机选择,所以后续提到的虚拟机都指HotSpot虚拟机。
【深入理解java虚拟机】会成为一个系列文章,来介绍JVM的方方面面,内容主要基于《深入理解java虚拟机-jdk1.7》一书(不用担心现在主流的是jdk1.8这回事,对jvm来说差距不大)辅助参考极客时间专栏《深入拆解Java虚拟机》的知识总结。我会按照如下的线索来讲述JVM虚拟机知识点:
- java代码如何编译成字节码文件,字节码文件中都包含什么。
- JVM有哪些内存区域,各个区域用来存放哪些信息,如何将字节码文件加载进JVM(类加载器),以及JVM中的异常处理。
- 虚拟机如何执行字节码指令的,以及JVM中的栈结构模型
- 虚拟机中的垃圾收集原理,以及常用的垃圾收集器。
- 运行时热点代码的即时编译
- java内存模型(如何解决共享变量的多线程同步问题)
- 线程安全与锁优化
以上的每一点都会是一篇独立的文章,这是第一篇,让我们先来弄明白java代码是怎样变成字节码文件的以及字节码文件究竟是什么。
javac编译器如何将java代码编译成字节码文件
各种不同的平台的虚拟机与所有平台都统一使用的程序存储格式——字节码是构成平台无关性的基石,虚拟机+字节码存储格式实现了虚拟机的语言无关性,java虚拟机不和包含java语言在内的任何语言绑定,它只和“Class文件”这种特殊的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。只要任何一门语言能按虚拟机的要求编译成字节码文件就能在虚拟机中运行。
从OracleJDK中的Javac代码来看,编译过程大致分为3个过程:
解析步骤包括了经典程序编译原理中的词法分析和语法分析两个过程,词法分析是将源代码的字符流转变为标记(Token)集合,标记是编译过程的最小元素,关键词、变量名、字面量、运算符都可以成为标记(如 “int a = b + 2”这句代码包含了6个标记,分别是 int 、a、=、b、+、2)。语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表中程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值、代码注释... 。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都会建立在抽象语法树上。
符号表是由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都会用到。在语义分析中符号表的内容将用于语义检查。填充符号表阶段还会对代码中未显式声明构造方法的类添加无参构造方法。
java语言提供对注解器的支持,注解处理器可以读取、修改、添加抽象语法树中的任意元素。
分析与字节码生成过程包含4个步骤,分别是 标注检查 数据及控制流分析 解语法糖 字节码生成。语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源代码的抽象,但无法保证源程序是符合逻辑的,而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查等。语义分析过程分为标注检查以及数据及控制类分析两个步骤。标注检查步骤的内容包括变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查中还会做一些常量折叠的优化(int a = 1 + 2 会被直接定义成int a = 3 )来减少程序运行时的CPU指令的运算量。数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理的问题。
使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。java中最常用到的语法糖有泛型、自动拆装箱、foreach等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的的基础语法结构。这个过程叫做解语法糖。
字节码生成是javac编译过程的最后一个阶段,这个阶段不仅仅是把前面各个步骤所生成的语法树、符号表转化成字节码写到磁盘中,还进行少量的代码添加和转换工作。例如实例构造器方法(指类中的 代码块和非静态成员变量赋值语句收敛到一个方法中)和类构造器方法(指类中的静态代码块和静态成员变量的赋值语句收敛到一个方法中)就是在这个阶段添加到语法树之中的。除此之外像将代码中的字符串加号拼接替换成StringBuffer的append()等等。完成对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表输出成字节码,生成最终的Class文件,到此整个编译过程完成。
字节码文件(Class类文件)中都有些什么
任何一个Class文件都对应着唯一一个类或接口的定义信息,Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的空间几乎全部是程序运行必要的数据,没有空隙存在。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,无符号数属于基本的数据类型,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型,用来描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。Class文件没有任何的分隔符所以所有的数据项无论是顺序还是数据量,甚至数据存储的字节序都是被严格限定的。
Class文件头4个字节称为魔数用来确定该文件是否为一个能被虚拟机接受的Class文件,接下来的4个字节是Class文件的版本号,java中高版本JDK能先下兼容以前的Class文件,但不能运行以后版本的Class文件就是根据这个版本标识确定的。
下图是用十六进制编辑器WinHex打开一个class文件的样子,给大家体会一下。
再接下来是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Clas文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,它是Class文件中第一个出现的表类型数据项。常量池中主要存放两大类常量:字面量和符号引用,字面量比较接近java语言层面的常量概念,如文本字符串、声明为final的常量值等。符号引用包含以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建时或者运行时解析、翻译到具体的内存地址上。
上面WinHex打开的class文件没办法直接解读,用javap命令可以对字节码文件进行分析,以一种可读的方式看到class文件中的内容,下面是一个简单的HelloWorld程序的Class文件用javap分析的结果,其中的Constant pool:部分就是Class文件中的常量池了。
常量池结束后下面的两个字节代表访问标志,包括这个Class是类还是接口、是否是public类型、是否定义为abstract类型、是否声明为final等。
访问标志之后是类索引、父类索引和接口集合,这些用来确定一个类的继承关系。再往下是字段表,用来描述接口或者类中声明的变量,包括类级别变量和实例变量,但不包含方法内部声明的变量。字段表之后是属性表集合用来存储一些额外的信息。再往下是方法表集合包括了方法的访问标志、名称索引、描述符索引、属性表索引,方法中的代码编译后变成字节码指令存放在方法属性表集合中的”Code“属性里。
上面提到属性表集合中的“Code属性”,属性表中的其他比较重要的属性还有“Exception属性”,用来列出方法中可能抛出的受查异常,也就是方法定义中throws关键词后面的异常类型。“InnerClasses属性”用于记录内部类与宿主类之间的关系。“Signature属性”用来记录泛型签名信息等等。
简单了解了属性表的内容,我们详细介绍一下其中最重要的一个属性也是整个Class中最重要的一个属性“Code”属性,上面提到Java程序方法体中的代码经过Javac编译器处理之后,最终变成字节码指令储存在方法表的属性表的Code属性中。如果把一个Java程序中的信息分为代码(方法体中的代码)和元数据(类、字段、方法定义等)两部分。那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项都用于描述元数据。上面那张图中你应该很容易找到Code属性的位置,试着看看里面的内容吧。其中args_size=1表示方法的参数个数是1(注意非静态方法即使代码中是无参的,这里会是1,其实是在隐式的传递this参数用来访问此方法所属的对象。而静态方法args_size会是0),
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
上面两句就是方法体中代码翻译出来的字节码指令了,如果方法中有显式的处理异常(try - catch)代码,这里还会有一个异常处理表集合来控制当程序出现指定异常应该怎么进行跳转(这个是指try-catch部分,前面说的“Exception属性”是记录throws部分信息,这里注意区分一下)。
字节码指令:
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有操作码。
《深入理解java虚拟机》中将字节码操作按照用途分为9类:
- 加载和存储指令:如iload fload aload istore bipush wide...
- 运算指令:如iadd lsub fmul ddiv irem lneg lushr ior land iinc dcmpg...
- 类型转换指令:如i2b l2i...
- 对象创建与访问指令:如new newarray arraylength...
- 操作数栈管理指令:如pop dup swap...
- 控制转移指令:如ifeq tableswitch goto...
- 方法调用和返回指令:如invokevirtual invokeinterface invokespecial invokestatic...
- 异常处理指令:如athrow...
- 同步指令:如monitorenter monitorexit...
更多指令以及指令的具体含义可以参照(http://www.blogjava.net/DLevin/archive/2011/09/13/358497.html 这里列举了java虚拟机中的指令)。到这里开篇的问题,java代码是怎样变成字节码文件的以及字节码文件究竟是什么,我们就说完了。这篇文章的初衷是让大家能对JVM有个总体的概念,而并不是详细的讲解虚拟机的实现细节,所以比较偏概念描述,后面的章节也会是这种风格。如果想了解一些细节问题可以到书中找寻答案。
确切的说javac编译器属于java虚拟机的外挂部分,并不是java虚拟机的组成部分。但是class文件是java虚拟机运行的”原料“,原料有了 下一篇我们切入到java虚拟机主体中,说一下虚拟机中都分哪些内存区域,以及类加载机制。
特别说明:文章中提到的JVM、java虚拟机、虚拟机等文字都是指java虚拟机,确切的说是指HotSpot虚拟机,这里说明一下不要对理解产生混乱。
猜你喜欢
- 2024-09-16 读Java性能权威指南(第2版)笔记08_即时编译器中
- 2024-09-16 Java @SuppressWarnings:抑制编译器警告-4
- 2024-09-16 PHP 8.0正式发布:支持JIT编译器,性能提升高达3倍
- 2024-09-16 学习廖雪峰的JAVA教程---泛型(擦拭法由编译器实现强制转型)
- 2024-09-16 JVM底层原理之如何选用C1、C2编译器?它们有什么区别?
- 2024-09-16 JIT编译器的神奇之处:为什么Java如此快速
- 2024-09-16 JVM底层原理之什么是JIT编译器?什么是HotSpot VM?
- 2024-09-16 C语言/C++/Java 入门到项目 资料和编译器
- 2024-09-16 如何在maven pom.xml文件中设置Java编译器版本
- 2024-09-16 Java后端精选基础教程:准备 Java 编译环境「连载 2」
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)