首页 > 编程知识 正文

深入理解java虚拟机第二版,深入理解java虚拟机看不懂

时间:2023-05-04 20:25:04 阅读:141648 作者:4675

文章目录序言1、前端编译阶段1 .前端编译做了什么? 2 .返回new一个对象2.1词法分析2.2语法分析2.3嵌入式符号表2.4插入式注释处理器2.4语义分析的注释检查2.5语义分析的数据流和控制流分析2.6语法分析糖2.6字节码生成2、代码执行1.1类加载过程1.1

首先,在调查new某个对象背后的故事之前,首先应该牢记两个概念。 而且,这两个概念有助于学习虚拟机的从头到尾。

前端编译:将*.java文件(我们程序员编写的代码文件)转换为*.class文件(虚拟机可以解释的格式文件)。

后端编译:将*.class文件转换为机器码文件(格式为计算机可读取和解释的文件)。

用以下代码开始虚拟机搜索之旅。

class GirlFriend{ private int age; 私有字符串名称; 公共girl friend (intage,String name ) { this.age=age; this.name=name; } public String getName () { return name; } class main { publicstaticvoidmain (string [ ] args ) girl friend bing=new girl friend (23,' bing bing ' ); system.out.println (bing.getname ); }一、前端编译阶段1 .前端编译做了什么? 前端编译是使用javac编译器将*.java文件转换为二进制代码文件*.class文件。 由于编译过程大致分为一个准备过程和三个处理过程,且理论抽象,故以案例分析如下。

准备步骤:初始化注释处理器

这里,您只需要知道注释处理器将注释的处理提前到了编译期间。 符号表的分析和嵌入过程

词法分析-将我们写的代码转换成一个个token (关键字、变量名、字面量、运算符等)的集合

语法分析-将上述token集合转换为语法树,概括了程序的体系结构。

填充-符号表包含符号地址和符号信息的键和值对。 用于程序编译的不同阶段。 例如,在生成程序时,为元件名称指定地址。 注释语义分析和字节码生成的处理

标注-包括是否声明了变量,变量和赋值之间的数据类型是否匹配。

数据流和控制流分析:进一步验证程序的上下文逻辑。

解开语法糖

字节码生成-将前一步骤中生成的信息转换为字节码指令并写入磁盘,只需少量的代码添加和转换。 2 .回到new对象后,我们将按照上面关于前端编译的说明,逐步分析代码在前端编译过程中发生了什么。

我们的代码很简单,不包含注释,所以忽略步骤1和步骤3。

2.1词法分析将我们编写的所有程序语句进一步细分,切割成token的集合。 因为每个程序语句都是我们编写程序的最小元素,而token是编译器编译的最小元素。

//词法分析前的private int age; //词法分析后,private、int和age 2.2语法分析通过词法分析得到了一些token的集合。 但是编译器至今仍不知道我们写的代码的逻辑结构。 因此,再将token集合归纳为树,描述程序的体系结构。 具体基于token流,利用TreeMaker,以JCTree的子类为语法节点构建抽象语法树。 语法树中的每个节点表示程序代码中的语法结构,包括包、类型、修饰符、运算符、接口和返回值。

代码:

package com.hou.test; 公共类girl friend { int age }; 字符串名称; 公共girl friend (intage,String name ) { this.age=age; this.name=name; } String getName () { return name; }抽象语法树:

2.3填充符号表的过程是将java类符号输入符号表中的过程。

该符号包含在字节码常量池中,主要包括该类使用的类、方法名称、包、接口和字段等的名称。

符号表是由符号信息和符号地址组成的一组表。

符号表填写过程是指将对应类的表中未出现的符号填写到表中。 记录在符号表中的信息可以用于编译的各种阶段,例如语义分析中的语义检查(检查名称的使用是否与原始声明一致)和目标代码生成阶段(对符号名称的地址分配)。

2.4通过图形注释处理器图形注释处理器,可以在编译阶段进行注释处理。

在评论处理中,我们可以得到所有的抽象语法树,并将其添加删除进行调查。 更改抽象语法树后,必须返回到第一级

段词法分析并从头开始。


通过上述过程,我们最终得到了一棵能表示整个程序并且能被虚拟机解读的抽象语法树。下面的工作就是对这个语法树的检查,以及将其进一步转化为可供虚拟机解释执行的字节码。
2.4 语义分析之标注检查

标注检查主要的工作就是检查变量使用前是否已经被声明(检查的依据就是上文提到的符号表)以及变量与赋值类型之间的数据类型能否匹配等工作。

2.5 语义分析之数据流与控制流分析

该部分的工作就是检查程序变量在使用前是否赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理等问题。
顺带一提,final关键字修饰的常量也是在这个时候被检查的,Class文件中局部变量没有访问标志,也就是说被final修饰的局部变量经过编译阶段后生成的字节码文件中是没有final的,因此只能在编译阶段才能被检查。

2.6 解语法糖

语法糖指的是在计算机语言中添加的某种语法,方便程序员使用,使用语法糖能够减少代码量、增加程序可读性、减少代码出错的机会,主要包括泛型、自动装箱、自动拆箱、条件编译等。
解语法糖就是虚拟机将减少的那部分代码量补充还原回来。

2.6 字节码生成

该阶段的主要工作是将经过语义分析检查后的抽象语法树生成字节码指令写入磁盘中,文件后缀名为.class,并且编译器还进行了少量的代码添加和转换,比如实例构造器<init>()方法和类构造器<clinit>()方法以及部分优化工作,如将字符串的加操作替换成StringBuffer或StringBuilder的append()操作。


到此为止,我们已经梳理了程序文件转换为二进制字节码文件的整个过程,也在磁盘中得到了该类的`.class`文件。
二、代码运行

通过编译我们得到了GrilFriend类的字节码文件,下面贴出我们的要执行的主函数:

class Main{ public static void main(String[] args) { GirlFriend bing = new GirlFriend(23, "bing bing"); System.out.println(bing.getName()); }}

主函数较为简单,只涉及new一个对象,调用该对象的getName()方法,并将结果打印。
需要说明的是,运行程序时虚拟机是从字节码文件中读取字节码指令并解释执行,另一种编译手段称之为即时编译,是将程序中经常出现的代码编译成本地机器代码,提高程序执行效率。本文代码简单,因此不涉及即时编译,故不做赘述。

1.类的加载过程

由于涉及到new操作,因此类的加载动作是不可避免的,在介绍具体的加载流程前先熟悉以下加载器。

1.1 类加载器

类加载器有两个作用,一是实现类的加载动作,二是确定类的唯一性。
类加载器和该类本身共同确定该类在虚拟机中的唯一性。即比较两个类是否相等的前提是二者是否是由同一个加载器加载出来的。

1.2 双亲委派模型


双亲委派模型生动得阐述了类加载器加载的过程,概括下来可以总结为两点:

类加载器收到类加载的请求,它首先不会自己加载,而是将请求委派给父类加载器,也就是说每次类加载的请求都会一直被委派直至启动类加载器。(自下向顶)各种类加载器负责加载的类的路径不同,也可以理解为优先级不同,当上面的加载器发现请求加载的类不在自己的加载范围,就开始让子加载器自己完成。(自顶向下)
此时我们已经确定出了要加载类的类加载器是哪一个,下面就是对该类的加载过程。
1.3 加载

当虚拟机解释执行到new GirlFriend(23, "bing bing")这里,通过new指令首先在方法区中寻找这个类是否被加载过,如果没被加载过则触发了类加载机制。
加载阶段虚拟机主要干三件事:

通过类的全限定名(GirlFriend)获取该类的二进制字节流。此处的二进制字节流不一定通过.class文件获取,也可以通过其他手段,例如网络、加密文件等。但该类经过编译已经生成了.class文件并保存在了磁盘中,因此我们只需要在磁盘中获取即可。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。此处将二进制字节流按照虚拟机所设定的格式存储在了方法区中。在堆中生成一个代表该类的Class对象,作为程序访问方法去中的类型数据的外部接口。这个也就是我们反射调用得到的Class对象了。 1.4 验证

这一步的主要目的是确保Class文件的字节流中包含的信息是否符合所有的约束要求,并保证这些信息运行后不会危害虚拟机的安全。
由于它起到一个保安的作用,所以它和加载是交叉执行的,即确定了文件安全,才允许加载。
验证阶段主要包括以下四个阶段的检验工作:

文件格式验证。验证字节流是否符合Class文件格式的规范。目的是保证输入的字节流能正确解析并存储在方法区内。元数据验证。主要涉及父类、接口、抽象类等的规范。字节码验证。目的是通过数据流及控制流分析,确保程序语义合法、符合逻辑,对类的方法体校验,确保方法在运行时不会危害虚拟机。符号引用验证。查看该类是否缺少或者被禁止访问它依赖的外部类、方法、字段等。 1.5 准备

该阶段为类中定义的变量分配内存并设置类变量初始值(零值)。

int age;String name;

准备阶段结束后,这两个类变量的值就分别是0和null。

1.6 解析

当一个类中调了外部的类,比如我们的代码中的Main类中调用了GirlFriend类,该类编译时,并不知道所引用的外部类的实际地址,因此只能用一个符号引用来代替。
那么解析阶段要做的工作就是将符号引用替换成直接引用。直接引用就是可以直接指向目标的指针。
此处的符号引用不单单指类的符号引用,还包括字段、方法的符号引用,但工作过程无非都是将符号引用转换为直接引用。但要注意的是解析时要先解析该字段或方法所在的类,搜索该类,如果没有对应的方法或字段就递归搜索父类或父接口。

1.7 初始化

初始化阶段就是执行类构造器<clinit>()方法的过程。以下是该方法的介绍:

编译器在编译过程中自动收集类变量的赋值动作、静态语句块,并将它们合并写入<clinit>()方法,编译结束后,该方法也生成完毕。虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法执行完毕。所以说第一个执行完的肯定是Object类的<clinit>()方法。如果一个类中没有静态语句块也没有类变量赋值动作,则编译器不生成该方法。接口和类略有不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口定义的变量被使用时,才会执行父接口的初始化。并且接口的而实现类在初始化时也不执行接口的<clinit>()方法。虚拟机确保了在多线程环境下一个类的<clinit>()方法被正确同步加锁。
此时类的整个加载过程已加载完毕,我们得到了堆区的一个`Class`对象,方法区的该类的加载信息。
2.继续往下走

此时我们刚刚结束了new GirlFriend(23, "bing bing")代码的分析,程序继续往下执行,在此之前先连接下java的内存区域划分。

方法区存储虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码缓存等数据。堆存储对象实例。程序计数器类似操作系统的寄存器,取指执行。虚拟机栈存储栈帧,每个栈帧对应着一个方法,栈帧内存储着局部变量表、操作数栈、方法出口等信息,栈帧入栈出栈的过程对应着方法调用执行和执行结束的过程。本地方法栈和虚拟机栈类似,只不过它使用的是本地方法。

接下来就可以很方便地用图来描述下面这一行代码。

GirlFriend bing = new GirlFriend(23, "bing bing");

GirlFriend创建一个对象引用,指向堆内存中的对象实例。
当我们调用该类的方法或字段时,就时通过对象引用找到对象实例,再从对象实例中寻找对应的字段或方法的过程。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。