热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

java虚拟机笔记(九)前端编译器和后端编译器

前端编译器:将java文件编译成class文件的编译器。java很多提高效率和友好度的语法糖都是依赖前端编译器实现的。javac就是一个典型的前端编译器。执行过程如下:1.准备阶段

前端编译器:
将java文件编译成class文件的编译器。
java很多提高效率和友好度的语法糖都是依赖前端编译器实现的。
javac就是一个典型的前端编译器。
执行过程如下:
1.准备阶段:
初始化插入式注解处理器
2.解析与填充符号表阶段:
词法 语法分析,将源代码转变为标记集合,构建抽象语法树
填充符号表,产生符号地址和符号信息。
3.插入式注解处理器的执行阶段:
会影响javac的行为,比如说添加一些编译期的校验等。
4. 分析与字节码的生成:
检查静态信息,数据流检查,解语法糖,字节码生成

词法解析:
将源代码的字符流转为标记(token)集合的过程,token是编译时的最小单位,关键字 变量名 运算符等都可以作为标记。

语法分析:
根据标记序列构造抽象语法树,抽象语法树是一种描述程序代码语法结构的树形表示,树的每一个节点代表程序代码中的一个语法结构,比如说
包,类型,修饰符,运算符等。

生成语法树之后,后续的操作都是对语法树进行操作。

填充符号表:
符号表是一组符号地址和符号信息构成的数据结构,当对符号名进行地址分配时,符号表是地址分配的直接依据。

注解处理器:
jdk5 时候提出了注解,原本设计上和普通java代码一样, 是程序运行期间发挥作用。但是在jdk6之后,设计了一组 插入式注解处理器的api,
可以提前至编译期对代码中的特定注解进行处理,当这些插件工作时,允许读取修改 添加抽象语法树中的任意元素。
如果插入式注解处理器对语法树进行过修改,那么编译器就会重新回到解析以及填充符号表的阶段。
比如说lombok的实现原理,就是通过插入式注解处理器来在编译期对特定的注解处理,生成get set方法等。

语义分析与字节码生成
抽象语法树能表示一个结构正确的代码,但是无法保证代码是符合逻辑的。
语义分析则是对结构上正确的程序进行检查,比如说类型检查,控制流检查,数据流检查。
比如说 int a=1; bool b=true; char c=a+b; 这种 ,就可以在语义分阶段被检查出来,
在IDE上看到报红下划线的错误,就是语义分析得出来的结果。

检查主要有:
1.标注检查
检查类型是否匹配,变量使用前是否声明等
2.数据和控制流分析
检查局部变量使用前是否赋值
方法的每条路径是否都有返回值
所有必检异常是否都被处理了

解语法糖:
语法糖,在计算机语言中添加某种语法,对编译和功能没影响,但是更方便程序员使用该语言。
java常见的语法糖有泛型,变长参数,自动装箱拆箱,等等,java虚拟机其实并不支持这些语法,这些语法会在编译期被还原回原始的样子。
泛型 obejct ,变长参数 数组参数, 自动装箱 拆箱 integer.valueof() 等

字节码生成;
将之前步骤所生成的信息,语法树 符号表等 转化成字节码指令。
除此之外,添加实例构造器()方法和 ()方法,这个动作是将{}里面的代码块都收集起来 放到()方法里面
或者将static{} 里面的代码都收集汇集到一起 放到()方法里面去。

java语法糖
语法糖是编程语言提高的一种能提高效率 或者减少编码出错机会的语法,由前端编译器进行解语法糖还原。

泛型:
java的泛型也是语法糖,实现方式叫类型擦除式泛型,编译期后就会被还原。
而C#使用的是具现化式泛型,这种泛型在源码里面,编译后,运行期 都是切实存在的
比如说List 和List 就是两个不同的类型,在运行时生成,都有各自的类型数据和虚方法表。
但是在java的泛型,只在程序源码中存在,在编译后的字节码文件中,都已经被还原为原来的裸类型,并在使用的地方加入了强制类型转换。
对于java来说,List和List在运行期就是同一个类型。

缺点:
1.性能低于具现式的泛型,因为擦除式泛型不支持基本类型 比如说int long ,要使用的时候就必须是要Intger Long
会造成不必要的拆箱装箱操作。
2.很多功能不支持,比如说创建泛型对象 new E(),判断一个实例是否属于泛型类型 xx instanceof E ,这些在C#中都支持。
优点:
3.运行时无法获取到类型信息,运行时要用到类型,就只能自己传入一个class类型作为参数进去。
4.泛型无法重载,比如说List 和List 无法作为方法重载的参数,
会被认为同一个参数类型List ,方法重载是静态分派,以静态类型判断走哪个方法,泛型擦除之后就无法判断了。

1.兼容jdk1.5以前的代码
2.不需要修改虚拟机,只需要新增javac编译器做个转换,影响范围小。

泛型历史:
设计成擦除式泛型是因为要兼容以前的jdk1.5之前无泛型的代码,也就是以前没有的限制不能以后突然冒出来。
比如说没有泛型之前,一个ArrayList列表,里面同时可以放interger又可以放String, 编译运行都不会报错。
现在的ArrayList 则必须指定里面元素类型了。
如果要兼容以前的,那么这个T 就必须是属于可有可无的,在编译期就会被擦除,变成ArrayListt,这样就和以前的一样了。
试想,如果T必须存在,那么以前的那种 一个ArrayList列表,里面同时可以放interger又可以放String的代码,如何自处? T到底是什么类型?

解决方法有两种,一个另起炉灶搞一套新的支持泛型的容器,另一个就是像现在的java这样。
c#使用了第一种方案,所以有两套容器,支持泛型的容器和老容器。
如果java也使用第一种方案,就会出现ArrayList,ArrayList 两套容器, 采用第二种方案之后,现在这两个容器其实是同一个。
比如说 ArrayList a 在编译之后就变成了ArrayList ,这个也就是裸类型,但是在元素方法的地方 加上了强制类型转换
(integer)a.get(0);

自动装箱/拆箱
将基本类型转为包装类型叫装箱,包装类型转基本类型叫拆箱。
Integer a=123;
其实发生了自动装箱,编译后是Integer a=Integer.valueof(123);
java虚拟机笔记(九)前端编译器和后端编译器

条件编译:
if条件为常量时候,必定为false的那段代码注定不会执行,会在编译器被消除掉。

插入式注解处理器:
lombok就是使用的插入式注解处理器实现的动态加get set方法。

插入式器的抽象类为javax.annotation.processing.AbstractProcessor。
子类必选实现process() ,这是javac编译器在执行注解处理器代码时候要调用的。
(javac编译器本身也是用java写的。)

代码如下:
@SupportedAnnotationType("*") //表示要支持的注解 *表示所有
@SupportedSourceVersion(SourceVesion.RELESASE_6) //支持的jdk版本
public class NameCheckProccessor extends AbstractProcessor{

@Override
public void init(ProcessingEnvironment processingEnv){
super.init(processingEnv);
//processingEnv 是注解处理器框架提供的一个上下文环境 要创建新的代码,向编译器输出信息 等都需要用到这个对象
}

@Override
public boolean process(Set annotations,RoundEnvironment roundEnv){
//annotations为要处理的注解集合
// roundEnv为该轮次的抽象语法树节点
//返回值为true 告诉编译器自己有改动语法树 需要重新解析
//返回false 则不需要

//对 roundEnv.getRootElements()进行循环遍历,查找自己要的节点 ,比如说 方法 ,类,字段 等。
// processingEnv的getMassager()方法可以返回一个Massager对象,该对象上有用于向编译器输出信息的方法

//比如说输出警告,massager.printMessage(Waring,"xxx ",e); e为当前元素elements

//jdk提供了一个抽象类,ElementScannerX X为jdk版本号, 继承这个类,可以以Visitor模式来访问抽象语法树上的元素。
比如说
for(Element e: roundEnv.getRootElements()){
XXXXElementScanner6(e); //XXXXElementScanner6为抽象类ElementScannerX的实现子类
}

}

ElementScannerX的抽象方法有用于访问类的visitType(TypeElemt e ,void p);
访问方法的 visitExecutable(ExecutableElemt e ,void p ); 方法变量的 visitVariable(VariableElemt e ,void p );

后端编译器:
将Class文件编译为与本地机器上可以识别的二进制指令的编译器。
主要有解释执行的解释器和编译执行的编译器。

目前主流的虚拟机都是解释器和编译器并存的运行架构,解释器和编译器各有优势。
当程序需要迅速启动时,解释器可以先发挥作用,省去编译的时间,立即运行。
当程序启动后,随着时间推移,编译器发挥作用,将越来越多的的代码编译成本地代码,获得更高的执行效率。

解释器使用内存比编译器略小,解释一行执行一行,而编译器需要将代码都编译成本地指令之后才执行。
解释器还可以充当编译器激进优化失败之后的备用方案。

hotspot虚拟机中有两个即时编译器,分别是客户端编译器和服务端编译器。
一般都是混合使用, -Xint 命令 强制使用解释器, java -Xcomp 优先使用编译器,编译无法进行的时候使用解释器。

即时编译需要占用程序运行时间,要编译出优化程度越高的代码,花费的时间就越长。
而且解释器需要采集性能监控信息给编译器,这对解释器效率也会有影响。
为了均衡在程序的启动响应速度和运行效率之间达成平衡,引入了分层编译的策略。

第0层,纯解释执行,而且解释器不开性能监控
第1层,使用客户端编译器将代码编译成本地代码,使用简单稳定的优化,不开启性能监控
第2层,使用客户端编译器,开启方法回边统计次数统计等有限的性能监控。
第3层,使用客户端编译器,开启方法回表统计次数和分支跳转,虚方法调用版本等性能监控
第4层,使用服务端编译器,使用更长时间的编译优化,根据性能监控进行进行激进不可靠的优化。

以上5层,在运行期间,虚拟机会根据运行参数和版本 进行调整。
实现分层编译之后,解释器 客户端编译器和服务端编译器 就可以都用上,客户端编译器获得更快的编译速度,服务端编译器获取更
优化的编译代码,解释执行时也不需要性能监控。 服务端编译器采用高复杂度的优化算法的时候,客户端编译器可以先用简单优化来为
它争取更多的时间。

编译对象与触发条件
会被即时编译器编译的代码是热点代码,热点代码是指
1.被多次调用的方法
2.被多次执行的循环体。

这两种情况下,编译的对象都是整个方法体。
如果是循环体触发的即时编译,叫栈上替换编译请求,这种编译方式,编译发生在方法执行过程中,叫栈上替换,
即方法的栈帧还在栈上,方法就被替换了。

那么如何知道代码是热点代码,就需要热点探测。
热点探测有两种:
1.基于采样的热点探测
周期性统计各个线程所在栈顶的方法出现的频率,如果某个方法经常出现在栈顶,说明经常被调用,就是热点方法。
优点是实现较简单,缺点是统计可能不准,会受线程阻塞影响等。

2.基于计数器的热点探测
为每个方法都建立一个计数器,统计方法的执行次数,一段时间内超过某个阈值就认为是热点方法,默认是10000次。
如果时间到了未到达阈值,就会将计数器减半,这个动作是在垃圾收集的时候顺便进行的,也可以通过设置命令-XX: -useCounterDecay
来关闭热度衰减,这样计数器调用的就是绝对次数,时间长了大部分代码都会被编译成本地代码。
-XX:CounterHalfLifeTime 设置半衰周的时间 单位为秒。

hotspot为每个方法建立了方法调用计数器和回边计数器(统计内部循环执行的代码)。 流程如下:
java方法入口,判断是否有已编译的版本,如果有,执行编译后的本地代码版本。
如果没有,那么方法调用计数器+1, 判断两个计数器之和是否超过阈值,
是,向编译器提交编译请求,然后用解释器执行,并不会等到编译器编译完成。
否,则直接用解释器执行。

回边计数器统计方法中循环体执行的次数。字节码上遇到控制流向后跳转的指令就叫回边,但是空循环不是回边,因为是自己跳自己,并没有向后跳。

当解释器遇到一个回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有的话,就会优先执行已编译的版本,
否则的话 回边指数器+1,然后判断两个计数器之和 是否满足阈值,如果满足,那么提交一个栈上替换编译请求,然后调小回边计数器的值以便当前继续在解释器中执行,然后使用调用解释器执行。这样的话,等到编译器编译好了,就可以使用编译版本执行了。

与方法调用计数器不同,回边计数器没有计数器衰减的过程,如果回边计数器满了,会把方法调用计数器也调满,这样下次就会触发标准编译过程。

编译过程:
无论方法调用产生的标准编译还是栈上替换编译请求,虚拟机在编译还没完成之前,都是用的解释器执行,后台有个编译线程,进行编译工作。
编译有三阶段:
一阶段:将字节码转为高级中间码,与目标机器指令集无关,这部分完成基础优化 如方法内联,常量传播等,特别是方法内联,是后续很多优化的基础。
二阶段:转为低级中间码,与目标指令集相关,这部分会完成空值检查消除,范围检查消除等
三阶段:寄存器分配,机器码生成。

提前编译器:
即时编译是在程序运行时将字节码编译成本地码,会占用用户程序的时间,提前编译是提前就将字节码编译成本地机器码。
优点就是不需要在运行时再去编译了。
但是缺点也很明显,缺少了运行时的性能数据采集支持,编译出来的代码优化程度不高,而且提前编译,会极大的延长启动时间。
提前编译也通常要求程序不能在外部动态加载新的字节码。

提前编译器有两种:
1.和c/c++一样,在程序运行之前就把程序代码翻译成机器码。
2.将原本即时编译器在运行时要做的编译工作提前做好并保存起来,下次运行到这部分代码,则可以直接使用,也叫编译缓存。

第一种,解决即时编译器要占用程序运行时间和运算资源。缺点就是增大了启动时间,在启动程序阶段就要将全部字节码编译成本地机器码。
第二种,本质上是给即时编译器做缓存,也叫动态提前编译或者即时编译缓存,这种提前编译的代码因为不能采用激进优化,性能要低于即时编译。

即时编译的优点:
1.性能分析
在运行过程中可以获取到信息,比如说某个抽象类通常是什么类型,条件判断通知会走哪个分支,方法调用使用哪个版本 等等
这些数据在静态分析是无法得到,但是在运行时就可以发现它们有非常明显的偏好性,那么就可以针对性的激进优化。

2.激进优化
即时编译的时候提供的性能信息 ,可以支持一些正确性很大但是无法保证绝对正确的优化,如果失败了就采用解释器兜底。
但是提前编译无法使用这种优化,必须保证百分百正确。
比如说在java中,大部分方法都是虚方法,只有静态方法 私有方法 构造方法不是虚方法, 如果java虚拟机遇到虚方法就去查方法表的话,就会很慢了。通常会通过类继承关系来做去虚拟化,这样可以保证虚方法也可以内联,进而可以优化。

3.链接时优化
在提前编译,静态编译是将不同的类库进行分别编译 优化。
但是在即时编译器中,所有的类都已经加载到了内存里面,可以关联优化,比如说调用某个库的方法,也可以进行方法内联等。


推荐阅读
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • mac php错误日志配置方法及错误级别修改
    本文介绍了在mac环境下配置php错误日志的方法,包括修改php.ini文件和httpd.conf文件的操作步骤。同时还介绍了如何修改错误级别,以及相应的错误级别参考链接。 ... [详细]
  • C++语言入门:数组的基本知识和应用领域
    本文介绍了C++语言的基本知识和应用领域,包括C++语言与Python语言的区别、C++语言的结构化特点、关键字和控制语句的使用、运算符的种类和表达式的灵活性、各种数据类型的运算以及指针概念的引入。同时,还探讨了C++语言在代码效率方面的优势和与汇编语言的比较。对于想要学习C++语言的初学者来说,本文提供了一个简洁而全面的入门指南。 ... [详细]
author-avatar
skychen88232011
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有