「深入理解java虚拟机」(一) - 编译器和字节码文件
mhr18 2024-10-04 17:14 19 浏览 0 评论
内容概要:
- 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虚拟机,这里说明一下不要对理解产生混乱。
相关推荐
- C++开发必知的内存问题及常用的解决方法-经典文章
-
1.内存管理功能问题由于C++语言对内存有主动控制权,内存使用灵活和效率高,但代价是不小心使用就会导致以下内存错误:omemoryoverrun:写内存越界odoublefree:同一块内...
- 缓存用不好,系统崩得早!10条军规让你成为缓存高手
-
凌晨三点,我被电话惊醒:“苏工!首页崩了!”监控显示:缓存命中率0%,数据库QPS10万+,线程阻塞2000+。根本原因竟是同事没加缓存!不会用缓存的程序员,就像不会刹车的赛车手——...
- 彻底搞清楚内存泄漏的原因,如何避免内存泄漏,如何定位内存泄漏
-
作为C/C++开发人员,内存泄漏是最容易遇到的问题之一,这是由C/C++语言的特性引起的。C/C++语言与其他语言不同,需要开发者去申请和释放内存,即需要开发者去管理内存,如果内存使用不当,就容易造成...
- Java中间件-Memcached(Java中间件大全)
-
一、知识结构及面试题目分析缓存技术的大规模使用是互联网架构区别于传统IT技术最大的地方,是整体高并发高性能架构设计中是重中之重的关键一笔,也是互联网公司比较偏好的面试题目。按照在软件系统中所处位置...
- linux内存碎片防治技术(linux内存碎片整理)
-
推荐视频:90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc原理剖析Linux内核内存分配与回收Linuxkernel组织管理物理内存的方式是buddysystem(伙...
- Redis主从架构详解(redis主从配置详细过程)
-
Redis主从架构搭建Redis主节点配置创建主节点目录(/opt/redis-master),复制redis.conf到该目录下,redis.conf配置项修改#后台启动daemonizeyes...
- 揭开CXL内存的神秘面纱(内存c1)
-
摘要:现代数据中心对内存容量的高需求促进了内存扩展和分解方面的多条创新线,其中一项获得极大关注的工作是基于ComputeeXpressLink(CXL)的内存扩展。为了更好地利用CXL,研究人员建...
- 一文彻底弄懂 TPS RPS QPS(tps cps)
-
以下是关于RPS、QPS、TPS的核心区别与关联的总结,结合实际场景和优化建议:一、核心定义与区别RPS:RequestsPerSecond每秒请求数客户端到服务器的完整请求数量Web服务...
- 用Redis的“集合”找出你和朋友的“共同关注”
-
你是不是在刷抖音、微博、小红书的时候,常常会看到这样的提示:“你和XXX有共同关注的博主/朋友”?或者当你关注了一个新的明星,系统会推荐“你的朋友YYY也关注了这位明星”?这个看似简单的功能背后,其实...
- WOT2016彭哲夫:科班出身开发者对运维人员的期许
-
“运维与开发”是老生常谈的话题,前几天和一个运维人聊天,TA说一些公司运维岗位都不公开招聘了,这让众多运维人员情何以堪?是运维的岗位真的饱和了?是找到合适的运维人才难?还是有这样那样的因素?带着这些疑...
- Java程序员最常用的20%技术总结(java程序员要掌握什么)
-
我听说编程语言,经常使用的是其中20%的技术。在Java这门语言中,这20%包括哪些内容?找到一份Java初级程序员的工作,有哪些是必须掌握的,有哪些是可以现学现卖的?一个完整的Javaweb项目,有...
- 秒杀系统实战(四)| 缓存与数据库双写一致性实战
-
前言微笑挖坑,努力填坑。————已经拥有黑眼圈,但还没学会小猪老师时间管理学的蛮三刀同学本文是秒杀系统的第四篇,我们来讨论秒杀系统中「缓存热点数据」的问题,进一步延伸到数据库和缓存的...
- 头条评论精灵翻牌子(头条评论精灵翻牌子怎么弄)
-
关于“头条评论精灵翻牌子”功能,这通常是指平台通过算法或运营手段,将用户的优质评论随机或定向推送到更显眼的位置(如信息流顶部、独立曝光位等),以提升互动率和用户参与感。以下是详细解析和建议:一、功能理...
- 15个程序员们都应该知道的大模型高级提示词指令模板和示例
-
作为程序员你如何写大模型指令?你写的指令是不是更专业呢?下面是15个程序员使用的专业的大模型指令,如果早知道可以能节省你很多时间。这些指令可以用在chatgpt,deepseek等大模型。1.一键...
- MyBatis-Plus内置的主键生成策略有大坑,要注意!
-
昨天小伙伴使用Mybaits-Plus开发的项目线上(集群、K8S)出现了主键重复问题,其报错如下:Mybatis-Plus启动时会通过com.baomidou.mybatisplus.core.to...
你 发表评论:
欢迎- 一周热门
-
-
Redis客户端 Jedis 与 Lettuce
-
高并发架构系列:Redis并发竞争key的解决方案详解
-
redis如何防止并发(redis如何防止高并发)
-
开源推荐:如何实现的一个高性能 Redis 服务器
-
redis安装与调优部署文档(WinServer)
-
Redis 入门 - 安装最全讲解(Windows、Linux、Docker)
-
一文带你了解 Redis 的发布与订阅的底层原理
-
Redis如何应对并发访问(redis控制并发量)
-
oracle数据库查询Sql语句是否使用索引及常见的索引失效的情况
-
Java SE Development Kit 8u441下载地址【windows版本】
-
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)