文章目录
JVM简介
Java虚拟机
维基百科,自由的百科全书
Java虚拟机概貌
Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。
最早由sun微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序。
Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
通过对中央处理器(CPU)所执行的软件实现,实现能执行编译过的Java程序码(Applet与应用程序)。
作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件符合JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。
此外,除了甲骨文,也有其他开源或闭源的实现
Java版本历史
JDK 与 JRE
JRE(Java Runtime Environment)
仅包含java运行的必要组件
- jvm
- java核心类库
JDK(Java Development Kit)
包含JRE外,还包含一系列开发,诊断工具
区别与联系
简单来说,JDK包含了JRE
如果只运行Java程序,只需要安装JRE即可
想要进行JDK开发 就需要安装JDK了
J2EE J2SE J2ME
1998年,JDK1.2发布,Java体系拆分为:
- J2SE(Java标准版)
- J2EE(Java企业版)
- J2ME(Java嵌入式版)
J2SE
Java 2 Standard Edition
主要用于桌面应用软件的编程
包含了构成Java语言核心的类
- jdbc
- 网络编程
- 接口定义
J2EE
Java 2 Enterprise Edition
包含了J2SE,再加上用于开发企业级应用的类
- Servlet
- JSP
- EJB
主要用于分布式的网络程序的开发
J2ME
Java 2 Micro Edition
包含了J2SE 中的一部分类
主要用于消费类电子产品的软件开发 手机 pad 寻呼机
Java SE Java EE Java ME
在2006年JDK1.6分布的时候,改名为
原名 J2SE J2EE J2ME
现名 JavaSE JavaEE JavaME
Java SE 6 \ Java SE 7 后面加上了版本号码
JDK 1.7 与 JDK 7
JDK 1.7 与 JDK 7 也是因为类似的命名更换导致的
版本历史
主条目:Java版本历史
- 1995年5月23日,Java语言诞生
- 1996年1月,第一个JDK-JDK1.0诞生
- 1996年4月,10个最主要的操作系统供应商申明将在其产品中嵌入JAVA技术
- 1996年9月,约8.3万个网页应用了JAVA技术来制作
- 1997年2月18日,JDK1.1发布
- 1997年4月2日,JavaOne会议召开,参与者逾一万人,创当时全球同类会议规模之纪录
- 1997年9月,JavaDeveloperConnection社区成员超过十万
- 1998年2月,JDK1.1被下载超过2,000,000次
- 1998年12月8日,JAVA2企业平台J2EE发布
- 1999年6月,SUN公司发布Java的三个版本:标准版(J2SE)、企业版(J2EE)和微型版(J2ME)
- 2000年5月8日,JDK1.3发布
- 2000年5月29日,JDK1.4发布
- 2001年6月5日,NOKIA宣布,到2003年将出售1亿部支持Java的手机
- 2001年9月24日,J2EE1.3发布
- 2002年2月26日,J2SE1.4发布,自此Java的计算能力有了大幅提升
- 2004年9月30日18:00PM,J2SE1.5发布,成为Java语言发展史上的又一里程碑。为了表示该版本的重要性,J2SE1.5更名为Java SE 5.0
- 2005年6月,JavaOne大会召开,SUN公司公开Java SE 6。此时,Java的各种版本已经更名,以取消其中的数字“2”:J2EE更名为Java EE,J2SE更名为Java SE,J2ME更名为Java ME
- 2006年12月,SUN公司发布JRE6.0
- 2009年12月,SUN公司发布Java EE 6
- 2010年11月,由于Oracle公司对于Java社群的不友善,因此Apache扬言将退出JCP[14]
- 2011年7月28日,Oracle公司发布Java SE 7
- 2014年3月18日,Oracle公司发表Java SE 8
- 2017年9月21日,Oracle公司发表Java SE 9
- 2018年3月21日,Oracle公司发表Java SE 10
- 2018年9月25日,Java SE 11发布
JVM版本历史
执行代码
分为
- 编译执行
- 解释执行
解释执行指的是边解释边运行代码。编译执行指的是先编译,后执行。
虚拟机始祖:Sun Classic
- Sun 发布 JDK 1.0,其中自带的虚拟机就是 Classic VM
- 只能使用纯解释器的方式来执行 Java 代码,,如果要使用 JIT 编译器那就必须使用外挂
- 外挂JIT编译器后,Sun Classic虚拟机速度不快,原因:解释器 编译器 无法配合工作
- 虽然Sun Classic虚拟机有很多问题,但是在JDK1.3前都是默认,1.3 默认改为HotSpot,1.4 退出。
短命Sun Exact Vm
- JDK1.2 发布Exact VM 尝试解决Classic解释器和编译器无法同事工作的问题,
- 但是一直都没有转正
霸主Sun HotSpot VM
- Longview Technologies 公司开发的,97年被Sun收购
- JIT编译上的许多优秀成果
- 准确式内存管理
- 热点探测技术:通过执行计数器找出最具有优化价值的代码,然后让JIT编辑器以方法为单位进行深度优化
- 1.3推出 现在仍在用
JVM历史
- 1996 年,JDK 1.0 发布时,提供了纯解释执行的 Java 虚拟机实现:Sun Classic VM。
- 1997 年,JDK 1.1 发布时,虚拟机没有做变更,依然使用 Sun Classic VM 作为默认的虚拟机。
- 1998 年,JDK 1.2 发布时,提供了运行在 Solaris 平台的 Exact VM 虚拟机,但此时还是用 Sun Classic VM 作为默认的 Java 虚拟机。
- 2000 年,JDK1.3 发布,默认的 Java 虚拟机由 Sun Classic VM 改为 Sun HotSopt VM,而 Sun Classic VM 则作为备用虚拟机。
- 2002 年,JDK 1.4 发布,Sun Classic VM 退出商用虚拟机舞台,直接使用 Sun HotSpot VM 作为默认虚拟机一直到现在。
虚拟机的工作
software
- Windows
.exe--------------------> Win code - Mac OS
.dmg--------------------> Mac code
不同的系统的软件无法互相安装
Java 如何做到一次编译到处运行哦?
java先编译成特定的语言规范 ByteCode
JVM再 将Bytecode编译成相应的Win Code…
准确地说,Java 虚拟机与字节码文件(Class文件)绑定
从Java到机器码的过程
无论什么语言 最后都要转化成机器码01,中间的过程值得探究
编译器可以分为:
- 前端编译器
- JIT编译器
- AOT
前端编译器:java到字节码
JDK安装目录里面有一个javac
编译:javac就是将java代码翻译成字节码
相对于其他编译器,它处于编译的前期,因此又被称为前端编译器
HelloWorld前端编译过程
public class HelloWorld{
public static void main(String args[]){
System.out.println("HelloWorld!");
}
}
javac HelloWorld.java
ls
HelloWorld.java HelloWorld.class
cat HelloWorld.class
以上就是使用javac编译器把Java语言规范转换为字节码规范
Javac编译四阶段
词法语法
在这个阶段,JVM会对源码的字符进行扫描,最终生成一个抽象的语法树
也就是说会分析java语言的语法
填充符号表
类之间会相互引用,但是编译阶段无法确定其具体的地址,所以会用一个符号代替。也就是对抽象的类或者接口进行符号化填充,等到类加载阶段,JVM会将符号替换成具体的内存地址
注解处理
Java当中的注解在这个阶段会进行处理,根据注解的作用将其还原成具体的指令集
分析与字节码生成
会根据以上阶段的结果,及逆行字节码的生成,最终输出class文件
我们一般称Javac编译器为前端编译器,因为发生在前端
常见的前端编译器:
javac
Eclipse JDT的增量式编译器(ECJ)
JIT编译器:从字节码到机器码
java-----------> 字节码 后
要运行程序 有两种选择
- java解释器解释执行字节码
启动速度快但是运行速度慢
边运行边解释
- 用JIT编译器将字节码转换成本地机器代码
启动速度慢运行速度快
启动时将所有的字节码解释成机器码
通常采用两者结合的方式进行
在HotSpot VM内置两个即时编译
- Client Compiler(C1编辑模式)
- Server Compiler(C2编辑模式)
C1 C2不是官方说法
C1 和 C2 的区别
实际上对于HotSpot来说 一共有三个模式可以选择
- 混合模式
C1 C2 两种模式混合起来使用(默认)
- 解释模式
所有的代码都执行
- 编译模式
先采用编译,但是无法编译的时也会解释执行
java -version可以看到
AOT编译器
直接将java-------------> 转换成 机器码
思想:
在程序执行前生成Java方法的本地code
以便在程序运行的时候直接使用本地code
但是java动态特性带来了额外的复杂性,影响了java程序静态编译代码的质量 例如 动态加载类
总结:
AOT从质量上看 比不上JIT
AOT存在的目的是在于避免JIT编译器的运行时性能消耗或者内存消耗,或者避免解释程序的早期性能开销
AOT编译速度比JIT编译出来慢 但是解释执行快
编译时间上,AOT也是一个始终的速度
AOT的存在就是JVM牺牲质量换取性能的一种策略
和JVM默认Mixed混合模式一样
使用C1 编译器只进行了简单的优化 C2优化较为激进
总结
JVM三个非常重要的编译器
- 前端编译器
- JIT
- AOT
前端最常见 javac
JIT 常见的就是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码
AOT能将java直接编译为机器码
编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
编译质量上,JIT 编译器 > AOT 编译器 > 解释执行
字节码文件结构
JVM使用字节码来实现了跨平台的愿景,无论什么系统 都可以使用JVM解释执行字节码文件。
字节码文件有一套规范,规定就是:《Java虚拟机规范》
《Java虚拟机规范》规定了:
- Java虚拟机结构
- Class类文件结构
- 字节码指令
字节码文件结构
是一组以8位字节为基础的二进制流,各数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何字符。在字节码结构中,有另种最基本的数据类型来表示字节码文件格式:无符号数和表
无符号数
以u1 u2 u4 u8 六七分别代表1个字节 2个字节 4个字节 8个字节和无符号数
无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值
例如 u4表示Class文件前4个字节表示该文件的魔数
第二行u2表示该Class文件第5-6个字节表示该JDK的次版本号
表
表是由多个无符号数字或者其他表作为数据项构成的复合数据类型
所有表都习惯的以 _info 结尾。
用于描述有层次结构的符合结构的数据
例如第五行表示其实一个类型为cp_info的表(常量池),这里存储了该类的所有常量
整个字节码文件本质上就是一张表
便于理解 将完整的表划分为以下七个部分
- 魔数与Class文件版本
- 常量池
- 访问标志
- 类索引 父亲索引 接口索引
- 字段表集合
- 方法表集合
- 属性表集合
Hello World编译
public static void HelloWorld{
public static void main(String args[]){
System.out.println("Hello World!");
}
}
javac HelloWorld.java
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
首先明确,字节码文件是用十六进(0x)制进行编码的
分析HelloWorld.claa
魔数与Class文件版本
1 - 4
Class文件的1 - 4个字节代表着魔数(Magic Number).它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件,其固定值是:0xCAFEBABE(咖啡宝贝)。如果一个Class文件的魔数不是这个,那么JVM将拒绝运行这个文件
5 - 6
Class文件的5 - 6 位代表了class文件的次版本号(Minor Version),即编译该Class文件的JDK次版本号
7 - 8
Class文件的第7 - 8个字节代表了Class文件的主版本号(Major Version),既编译该Class文件的JDK主版本号
JDK能够向下兼容 向上不兼容
我们看看之前的 Demo 文件的 Class 文件内容,其前 8 个字节分别是:cafe babe 0000 0034
对比上面表格中的数据,那么我们可以知道,这个 Class 文件是由 JDK1.8 编译的
常量池
紧跟版本信息的是常量池信息
前两个字节表示常量池个数
后面不定长数据表示常量池的具体信息
从图中看 常量池通常由cp_info这种结构组成的,而且表结构不同大小也不同。在JVM规范中一共有14种cp_info类型的表结构
上面这些cp_info表结构又有不同的数据结构,对应数据结构如下
cp_info表结构一共有三个字段
- 表结构的标识值,一个字节大小,对应数字
- 结构描述
- 结构描述 不同于2
Hello world
Hello World 文件字节码对应的内容是:00 1d,其值为 29,表示一共有 29 - 1 = 28 个常量
紧跟着常量池的就是 28 个常量了,因为每个常量都对应不同的类型,所以我们无法得知其具体大小,只能一个个分析
第一个常量
紧接着 001d 的后一个字节为 0A,为十进制数字 10,查表可知其为方法引用类型(CONSTANT_Methodref_info)的常量
再查cp_info对应的表结构知道,该常量第 2 - 3 个字节表示类信息, 4 - 5 个字节表示名称及类描述
接下来我们取出这部分的数据:0a 0600 000f
该常量项第 2 - 3 个字节,其值为 00 06,表示指向常量池第 6 个常量所表示的信息。根据后面我们分析的结果知道第 6 个常量是 java/lang/Object。第 4 - 5 个字节,其值为 000f,表示指向常量池第 15 个常量所表示的信息,根据 javap 反编译出来的信息可知第 10 个常量是 <init>:()V
。将这两者组合起来就是:java/lang/Object.<init>:V
,即 Object 的 init 初始化方法
…
运行javap -verbose HelloWorld.class
,可以打印出Class文件的构成信息,其中就包括了常量池信息
访问标志(access_flag)
常量池结束后,紧接着的两个字节代表类或接口的访问标记(access_flag)
这里的数据为0021
这个标志用于识别一些类或者接口层次的访问信息,包括:
- 这个Class是类还是接口
- 是否定义为public类型
- 是否定义为abstract
…
在这里这两个字节是 00 21,通过查看我们并没有发现有标志值是 00 21 的标志名称。这是因为这里的访问标志可能是由多个标志名称组成的,所以字节码文件中的标志值其实是多个值进行或运算的结果。
通过查阅上述表格,我们可以知道,00 21 由 00 01(第1行)和 00 20(第3行)进行或运算得来。也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义
类索引、父类索引、接口索引
这里数据为:00 05 00 06 00 00
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系
类索引
类索引用于确定这个类的全限定名,用一个u2类型的数据显示。类索引005指向了常量池的第五个常量(HelloWorld类)
父类索引
确定这个类的父类的全限定名字,父类引用一个u2类型的数据表示。这里父类索引是0006表示其指向了常量池中的第6个常量,之前知道第6个常量最终的信息是Object类
接口索引
描述类实现了那些接口,这些被实现的接口按照implements语句(如果这个类本身就是接口,应该按照extend语句)后的接口顺序从左到右排列再接口索引集合中。对于接口索引集合,入口为计数器(interfaces_count)表示索引容量,而在接口计数器后紧跟着所有的接口信息。若该类没有实现任何接口,计数器为0,后面的接口索引表不在占用任何字节
HelloWorld.class中 没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没事实现任何接口
字段表集合
字段表集合用于描述接口或者类中声明的变量,这里的数据为 00 00
这里说的字段包括类级别变量和实例级别的变量,但不包括在方法内部声明的局部变量。在类接口后的2个字节是一个字段计数器,表示总有几个属性字段,在字段计数器后,才是具体的属性数据
字段表的每一个字段用一个filed_info的表来表示
field_info表的数据结构如下:
因为没有声明任何的类成员变量或类变量,因此 00 00 ,表示没有属性字段
方法表集合
该字段表后的2个字节是一个方法计数器:类中总共有几个方法
字段计数器后,才是具体的方法数据:00 02
方法表中每一个方法都用一个method_info表示,数据如下
0002表示只有两个方法
第 1 个方法,这里数据为:00 01 00 07 00 08 00 01 00 09 00 0000 1d 00 01 00 01 00 0000 05 2a b7 00 01 b1 0000 0001 000a 0000 0006 0001 0000 0001
方法计数器后 2 个字节表示方法访问标识,这里是 00 01,表示其实 ACC_PUBLIC 标识,对比上面的图表可知其表示 public 访问标识。紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了<init>
。紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V
紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法的属性表一共有 1 个属性。属性表的表结构如下
前两个字节是名字索引、接着 4 个字节是属性长度、接着是属性的值。这里前两个字节为 0009,指向了常量池第9个常量,查询可知其值为Code,说明此属性是方法的字节码描述。 Code 属性的表结构如下
根据 Code 属性对应表结构知道,前 2 个字节为 0009,即常量池第 9 个常量,查询知道是字符串常量Code。接着 4 个字节表示属性长度,这里值为 1D,即 29 的长度。下面我们继续分析 Code 属性的数据内容。
紧接着 2 个字节为 max_stack 属性。这里数据为 00 01,表示操作数栈深度的最大值。
紧接着 2 个字节为 max_locals属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
接着 4 个字节为 code_length,表示生成字节码这里给的长度。这里数据为 00 00 00 05,表示生成字节码长度为 5 个字节。那么紧接着 5 个自己就是对应的数据,这里数据为 2a b7 00 01 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令:
- 读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
- 读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
- 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器“”方法的符号引用。
- 读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束
接着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。
紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。attribute_info 属性表的表结构如下
首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。LineNumberTable 表的表结构如下图所示
接着 4 个字节是属性长度,这里数据为 00 00 00 06,表示有 6 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 01,表示长度为 1。接着跟着 1 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号
那么接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 01,即 line_number 表示 Java 源码行号为第 1 行。
到此,我们方法表集合的第一个方法分析结束。我们通过 javap 反编译查看,可以看到 Code 和 LineNumberTable 都是完全正确的。
属性表集合
这里或许有人会迷惑,上面我们不是分析过属性表了么。其实上面分析的是方法中的属性,而这个是类中的属性。这个就像局部变量和类成员变量一样,是不同的。
紧接着我们剩下的数据为:00 0100 0d00 0000 0200 0e,这些就是属性表集合的数据了
根据上面的表格我们知道,紧跟着的 2 个字节数据是属性表属性数量,这里数据为 00 01,表示有 1 个属性。后面紧跟着 1 个表结构为 attribute_info 的属性数据。attribute_info 表的结构如下图所示
前两个字段为属性名称索引,这里数据为 00 0d,表示第 13 个常量池,查询可知这里的值是:SourceFile。SourceFile 属性的表结构如下图所示
SourceFile 表结构前两个字节我们已经分析过,数据为 00 0d,表示第 13 个常量池,指的是SourceFile这个值。接着我们看后面 4 个字节,这里数据为 00 00 00 02,表示属性长度为 2 个字节。紧跟着的 2 个字节表示 SourceFile 的常量池索引,即该字节码文件的源文件名称,这里数据是 00 0e,即常量池的第 14 项,即Demo.java。所以这个属性项标识了该字节码文件的源文件名称为 Demo.java。