代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
-
Class文件结构(Class文件结构比较复杂,具体的表结构需要的时候自行查阅即可)
Class文件结构采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本数据类型,比如u1,u2,u4,u8分别代表占一个字节,两个字节,四个字节,八个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串值;表是由多个无符号数或者其他表作为数据项构成的复合数据结构,所有表都习惯性的以“_info”结尾。
- magic(魔数)u4
每个Class文件的头四个字节称为魔数,他的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,相比于扩展名更为安全,因为扩展名可以随便更改。 - minor_version(次版本号) u2
- major_version(主版本号)u2
- constant_pool_count(常量池容量计数值) u2
从1开始计数,而不是0,比如22就表示有21项常量,将第一个空了出来。 - constant_pool (常量池)cp_info
常量池主要存放两大类常量:字面量和符号引用。字面量类似于Java的常量如字符串,符号引用是编译原理里的概念,包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。jdk1.7之后常量池中共有14种结构各不相同的表结构,常量池是很复杂的一部分,具体可以查看class文件。 - access_flags (访问权限标识) u2
这两个字节主要保存了class的基本信息,比如Class是类还是接口,是否为public,是否为final,是否为Abstract等等 - this_class (类索引) u2
存储这个类的全限定名 - super_class (父类索引)u2
存储这个类的父类全限定名,除了Object外,所有Class都有父类,而且只有一个 - interfaces_count(接口索引集合计数值)u2
从0开始,表示这个类实现了几个接口 - interfaces (接口索引集合)u2
分别存储了,这个类实现的接口的全限定名 - fields_count (字段表集合计数)u2
表示这个类里有几个字段(不包括局部变量) - fields (字段表集合)filed_info
用于描述接口或类中声明的字段,比如这个字段的作用域,是否statis,是否final,是否volatile,是否transient,字段数据类型,字段名称等。不会出现从父类继承而来的字段。 - methods_count (方法表集合计数)u2
表示这个类有几个方法 - methods (方法表集合)method_info
用于描述方法和字段描述几乎相同,但是方法里的代码经过编译器成字节码指令后,存放在方法属性表集合中一个叫“Code”的属性中。此外如果父类方法在子类中没有被重写就不会出现,由于重载方法,要求拥有不同的特征签名,但是方法返回值不在特征签名里包含,所以Java不支持返回值重载方法。 - attributes_count (属性表集合计数)u2
属性表有几个 - attributes (属性表集合)attribute_info
在Class文件,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息,jdk7之后属性已经增加到了21项,比如之前提到的Code属性,以及Exception属性等等,需要的时候自行查阅需要的属性表结构,不在此赘述。(可以看看Code属性是如何执行指令的)
- magic(魔数)u4
-
字节码指令简介,这部分看一下,方便阅读字节码,不过大部分都可以由语义推断出来。
-
类生命周期
-
类何时初始化(类何时加载,是由虚拟机自己决定的)
- 遇到new,getstatic,putstatic,invokestatic四条指令会触发类初始化
- 进行反射调用的时候会触发类初始化
- 初始化一个类,如果其父类未进行初始化,则会触发其类的初始化
- 虚拟机启动的时候,包含main方法的类会触发初始化
- 如果java.lang.invoke.MethodHandle实例之后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这方法句柄对应的类没有初始化会触发初始化。
其他情况下都属于被动引用,不会触发类的初始化,如常量传播优化机制。
-
类加载过程
加载,验证,准备,解析,初始化。- 装载
加载是类加载的一个阶段,通过全限定名来获取类对应的二进制字节流,将这个字节流的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法去的访问入口。 - 验证
验证是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括文件格式检验,元数据检验,字节码验证,符号引用验证。这步是基于二进制字节流的验证,只有通过这步的验证,字节流才会进入方法区存储,并且后续不会再操作字节流。
文件格式验证包括,魔数校验,版本号校验,常量合法校验
元数据验证是验证否违法Java语法,比如是否有父类,是否继承了final类
字节码验证是通过数据流和控制流分析,确定语义是合法的且符合逻辑的
符号引用验证是对常量池的符号引用验证,比如是否能找到对应的引用, - 准备
准备是正式为类变量分配内存并设置类变量初始值的阶段,这些变量都再方法区里分配内存。final是再准备阶段赋值的。 - 解析
解析阶段是将常量池内的符号引用替换为直接引用的过程,具体解析过程基本就是先查自己,自己没有递归查父类,最后去接口中找。(这里明白理解符号引用和直接引用的区别) - 初始化
初始化是执行类构造函数的过程,这时候才会执行用户的代码,为变量赋初值等等。
- 装载
-
类加载器和双亲委派模型
注意:只有被同一个类加载器加载的Class才存在相等的概念。
双亲委派模型要求,除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。所有类加载器收到加载请求,都会先发给自己的父类尝试加载,只有父类反馈自己无法完成加载的时候,子类加载器才会尝试自己来加载。通过双亲委派模型保证所有的相同的Class文件都被同一个类加载器加载,避免混乱。
双亲委派模型被破坏过三次,第一次是为了兼容老的jdk1.0代码;第二次是为了解决spi机制的问题,这是双亲委派模型的缺陷;第三次是为了实现热部署这样的方便操作。
-
栈帧结构
栈帧在内存管理模块就出现过,但是当时没有详细讲。栈帧包括局部变量表,操作数栈,动态连接,方法返回地址以及一些额外的附加信息。- 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。基本存储单元是Slot,没有明确指定大小,但是应该都能放下JVM的任意一种数据类型。(JVM数据类型和Java的数据类型有本质区别,JVM数据类型有boolean,byte,char,short,int,float,reference,returnAddress,前六种可以类比Java的类型理解,reference类型很重要,Java虚拟机规定其一引用必须做到通过引用找到对象在堆中的起始地址,其二引用必须要能找到对象所属Class信息在方法区 的位置,这是Java支持反射而C++不支持反射的根本原因,retuanAddress之前用于异常处理,现已经被异常表代替了)
- 操作数栈,类似于用stack实现复杂表达式的过程,执行指令。
- 动态连接,存储一个指向运行时常量池中该栈帧所属方法的引用,为了实现方法调用的动态连接。
- 方法返回地址,存储一些帮助恢复上层方法的信息,以维持方法继续执行
-
Java为什么是静态多分派,动态单分派语言
Java的方法调用提供了五个指令,分别是invokestatic,invokespecial,invokevirtual,invokeinterface,invokedynamic,被后三种指令调用的方法被称为虚方法(注意,final方法虽然是使用invokevirtual指令调用,但是Java规范明确指出final方法是非虚方法)对于静态分派和动态分派,非虚方法的分派属于静态分派,一般的表现形式是重载;对于虚方法的分派属于动态分派,一般的表现形式是多态。动态和静态的根本区别在于是在运行时还是编译时确定调用的方法。为了优化动态分派,JVM在方法区建立了虚方法表。
对于单分派和多分派,是根据方法的宗量来判断的,方法的参数和接收者统称为方法的宗量。单分派是仅根据一个宗量来判断,多分派是根据多个宗量来判断。而静态分派,显然可以根据方法的接收者和参数来选择,自然是支持多分派的;但是对于动态分派,也就是执行invokevirtual这条指令的时候,方法的签名已经确定了,所以虚拟机不再关心传递的参数类型,唯一影响虚拟机的选择因素只有此方法的接收者的实际类型,只根据一个宗量来选择方法,所以是单分派。所以Java是一种静态多分派,动态单分派的语言。
-
Java对动态类型语言支持*
首先要明确动态类型语言和动态语言的概念。
主要通过两种方式来支持,一种是java.lang.invoke包下面的MethodHandle,另一种是通过invokeddynamic指令(这里可以看作者写的demo理解下,这是Java动态特性的底层支持,目前理解也不够深入)
MethodHandle和Reflection的区别:Reflection是在代码级别的模拟方法调用,MethodHandle则是字节码层次的;Reflection是重量级的,MethodHandle是轻量级的;MethodHandle可以支持方法内联等,Reflection不行。 -
Java程序如何执行
Java代码执行方式有两种,一种是编译执行,就是将通过即时编译器将Class文件进一步编译成本地机器码执行,用物理机来直接执行,效率高,但是编译需要耗时长;另一种是解释执行,即虚拟机直接对字节码的指令进行执行,解释执行使用的指令集一般有基于栈的指令集和基于寄存器的指令集。前者可移植性强,后者执行速度比较快。
这部分主要讲了Java的代码编译之后的Class类文件结构,然后如何加载到JVM中,以及如何对字节码执行。