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

开发笔记:来自阿里P7的分享!!带你彻彻底底的弄懂Java虚拟机

篇首语:本文由编程笔记#小编为大家整理,主要介绍了来自阿里P7的分享!!带你彻彻底底的弄懂Java虚拟机相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了来自阿里P7的分享!!带你彻彻底底的弄懂Java虚拟机相关的知识,希望对你有一定的参考价值。






明人不说暗话 纯干货开始分享!!!!

BaronTalk 对于性能和效率的追求一直是程序开发中永恒不变的宗旨,除了我们自己在编码过程中要充分考虑代码的性能和效率,虚拟机在编译阶段也会对代码进行优化。本文就从虚拟机层面来看看虚拟机对我们所编写的代码采用了哪些优化手段。


Javac编译器

本身是一个由Java语言编写的程序,代码存放在tools.jar中,从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程

准备过程:初始化插入式注解处理器。

处理过程:


  • 解析与填充符号表过程。
  • 插入式注解处理器的注解处理过程
  • 分析与字节码生成过程

上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转 回到之前的解析、填充符号表的过程中重新处理这些新符号


1.解析与填充符号表

解析过程包括了经典程序编译原理中的词法分析和语法分析两个步骤
1.词法、语法分析
词法分析是将源代码的字符流转变为标记(Token)集合的过程,单个字符是程序编写时的最小元 素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记,如“int a=b+2”这句代码中就包含了6个标记,分别是int、a、=、b、+、2,虽然关键字int由3个字符构成,但 是它只是一个独立的标记,不可以再拆分。在Javac的源码中,词法分析过程由 com.sun.tools.javac.parser.Scanner类来实现。
语法分析是根据标记序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一 种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个 语法结构(Syntax Construct),例如包、类型、修饰符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。
经过词法和语法分析生成语法树以后,编译器就不会再对源码字符流进行操作了,后续的操作都 建立在抽象语法树之上。
大家在IDEA上面搜索JDT AstView插件,右键Enable JDT AST View 可以查看当前代码的抽象语法树结构,如下图


2.填充符号表

完成了语法分析和词法分析之后,下一个阶段是对符号表进行填充的过程。
符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构,读者可以把它类比想象成哈希表中键值对的存储形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表、栈结构符号表等各种形式)。符号表中所登记的信息在编译的不同阶段都要被用到。譬如在语义分析的过程中,符号表所登记的内容将用于语义检查 (如检查一个名字的使用和原先的声明是否一致)和产生中间代码,在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。

2.插入式注解处理器的注解处理过程

插入式注解处理器可以看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法 树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有 再对语法树进行修改为止,每一次循环过程称为一个轮次(Round)。
例如Java著名的编码效率工具Lombok,它可以通过注解来实现自动产生 getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()方法就是依赖插入式注解处理器来实现的。


3.语义分析与字节码生成

经过语法分析之后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正 确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源 程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,等等。

int a = 1;
boolean b = false;
char c = 2

后续可能出现的赋值运算:

int d = a + c;
int d = b + c;
char d = a + c;
//加入Java开发交流君样:756584822一起吹水聊天

上面的编码IDEA 中看到由红线标注的错误提示,其中大部分都是来源于语义分析阶段的检查结果。
Javac在编译过程中,语义分析过程可分为标注检查和数据及控制流分析两个步骤


1.标注检查

标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否 能够匹配,等等,刚才3个变量定义的例子就属于标注检查的处理范畴。在标注检查中,还会顺便进行一个称为常量折叠(Constant Folding)的代码优化,这是Javac编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)【参考文献】


2.数据及控制流分析

数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问 题。编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上可以看作是一致的, 但校验范围会有所区别,有一些校验项只有在编译期或运行期才能进行。
3.字节码生成
字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令 写到磁盘中,编译器还进行了少量的代码添加和转换工作。
完成了对语法树的遍历和调整之后,输出字节码,生成最终的Class 文件,到此,整个编译过程宣告结束。


Java语法糖

通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
Java中最常见的语法糖包括了泛型、变长参数、自动装箱拆箱,等等,Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。
1.泛型
泛型的本质是参数化类型(Parameterized Type)或者参数化多态(Parametric Polymorphism)的应用。
Java泛型实现方式叫作“类型擦除式泛型”(Type Erasure Generics)

Map<String, String> map &#61; new HashMap<String, String>();
map.put("hello", "你好");
System.out.println(map.get("hello"));

—编译成Class文件&#xff0c;然后再用字节码反编译工具进行反编译后&#xff0c;发现泛型都不见了-----------

Map map &#61; new HashMap();
map.put("hello", "你好");
//在元素访问时插入了从Object到String的强制转型代码
System.out.println((String) map.get("hello"));

擦除法所谓的擦除&#xff0c;仅仅是对方法的Code属性中的字节码进行擦除&#xff0c;实际上元数据中还是保留了泛型信息&#xff0c;这也是我们在编码时能通过反射手段取得参数化类型的根本依据。
2.自动装箱、拆箱与遍历循环

List<Integer> list &#61; Arrays.asList(1, 2);
int sum &#61; 0;
for (int i : list) {
sum &#43;&#61; i;
}//加入Java开发交流君样&#xff1a;756584822一起吹水聊天
---------------------------------编译之后--------------------------------
List list &#61; Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2) });
int sum &#61; 0;
for (Iterator localIterator &#61; list.iterator(); localIterator.hasNext(); ) {
int i &#61; ((Integer)localIterator.next()).intValue();
sum &#43;&#61; i;
}

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法&#xff0c;上面的的Integer.valueOf()与Integer.intValue()方法&#xff0c;而遍历循环则是把代码还原成了迭代器的实现&#xff0c;这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。


后端编译与优化

字节码看作是程序语言的一种中间表示形式&#xff08;Intermediate Representation&#xff0c;IR&#xff09;的话&#xff0c; 那编译器无论在何时、在何种状态下把Class文件转换成与本地基础设施&#xff08;硬件指令集、操作系统&#xff09;相关的二进制机器码&#xff0c;它都可以视为整个编译过程的后端。
即时编译器【参考文献】

通过解释器 &#xff08;Interpreter&#xff09;进行解释执行的&#xff0c;当虚拟机发现某个方法或代码块的运行特别频繁&#xff0c;就会把这些代码认定为“热点代码”&#xff08;Hot Spot Code&#xff09;&#xff0c;为了提高热点代码的执行效率&#xff0c;在运行时&#xff0c;虚拟机将会把这些代码编译成本地机器码&#xff0c;并以各种手段尽可能地进行代码优化&#xff0c;运行时完成这个任务的后端编译器被称为即时编译器。


解释器与编译器

HotSpot虚拟机中内同时包含解释器与编译器&#xff0c;当程序启动后&#xff0c;随着时间的推移&#xff0c;编译器逐渐发挥作用&#xff0c;把越来越多的代码编译成本地代码&#xff0c;这样可以减少解释器的中间损耗&#xff0c;获得更高的执行效率。当程序运行环境中内存资源限制较大&#xff0c;可以使用解释执行节约内存&#xff08;如部分嵌入式系统中和大部分的JavaCard应用中就只有解释器的存在&#xff09;&#xff0c;反之可以使用编译执行来提升效率。因此在整个Java虚拟机执行架构里&#xff0c;解释器与编译器经常是相辅相成地配合工作。
【参考文献】


由于即时编译器编译本地代码需要占用程序运行时间&#xff0c;通常要编译出优化程度越高的代码&#xff0c;所花 费的时间便会越长&#xff1b;而且想要编译出优化程度更高的代码&#xff0c;解释器可能还要替编译器收集性能监控信 息&#xff0c;这对解释执行阶段的速度也有所影响。为了在程序启动响应速度与运行效率之间达到最佳平衡&#xff0c;
HotSpot虚拟机在编译子系统中加入了分层编译的功能&#xff1a;


  • 第0层。程序纯解释执行&#xff0c;并且解释器不开启性能监控功能&#xff08;Profiling&#xff09;。
  • 第1层。使用客户端编译器将字节码编译为本地代码来运行&#xff0c;进行简单可靠的稳定优化&#xff0c;不开启 性能监控功能。
  • 第2层。仍然使用客户端编译器执行&#xff0c;仅开启方法及回边次数统计等有限的性能监控功能。
  • 第3层。仍然使用客户端编译器执行&#xff0c;开启全部性能监控&#xff0c;除了第2层的统计信息外&#xff0c;还会收集如 分支跳转、虚方法调用版本等全部的统计信息。
  • 第4层。使用服务端编译器将字节码编译为本地代码&#xff0c;相比起客户端编译器&#xff0c;服务端编译器会启 用更多编译耗时更长的优化&#xff0c;还会根据性能监控信息进行一些不可靠的激进优化。【参考文献】

实施分层编译后&#xff0c;解释器、客户端编译器和服务端编译器就会同时工作&#xff0c;热点代码都可能会被多 次编译&#xff0c;用客户端编译器获取更高的编译速度&#xff0c;用服务端编译器来获取更好的编译质量&#xff0c;在解释执行 的时候也无须额外承担收集性能监控信息的任务&#xff0c;而在服务端编译器采用高复杂度的优化算法时&#xff0c;客 户端编译器可先采用简单优化来为它争取更多的编译时间。




标题编译对象与触发条件

上面提到的即时编译器编译的目标是“热点代码”&#xff0c;这里所指的热点代码主要有两类&#xff1a;

被多次调用的方法。
被多次执行的循环体。

一个方法被调用得多了&#xff0c;方法体内代码执行的次数自然就多&#xff0c;它成为“热点代 码”是理所当然的。而后者则是为了解决当一个方法只被调用过一次或少量的几次&#xff0c;但是方法体内部存 在循环次数较多的循环体&#xff0c;这样循环体的代码也被重复执行多次&#xff0c;因此这些代码也应该认为是“热点代 码”

第一种情况&#xff0c;由于是依靠方法调用触发的编译&#xff0c;那编译器理所当然地会以整个方法作为编译对象&#xff0c;这种编译也是虚拟机中标准的即时编译方式。而对于后一种情况&#xff0c;尽管编译动作是由循环体所触发的&#xff0c;热点只是方法的一 部分&#xff0c;但编译器依然必须以整个方法作为编译对象&#xff0c;只是执行入口&#xff08;从方法第几条字节码指令开始执 行&#xff09;会稍有不同&#xff0c;编译时会传入执行入口点字节码序号&#xff08;Byte Code Index&#xff0c;BCI&#xff09;。这种编译方式因为编译发生在方法执行的过程中&#xff0c;因此被很形象地称为“栈上替换”&#xff08;On Stack Replacement&#xff0c;OSR&#xff09;&#xff0c;即方法的栈帧还在栈上&#xff0c;方法就被替换了。

要知道某段代码是不是热点代码&#xff0c;是不是需要触发即时编译&#xff0c;这个行为称为“热点探测”&#xff08;Hot Spot Code Detection&#xff09;。
HotSpot 虚拟机中使用的是基于计数器的热点探测&#xff0c;每个方法&#xff08;甚至是代码块&#xff09;建立计数器&#xff0c;统计方法的执行次数&#xff0c;如果执行次数超过一定的阈值就认为 它是“热点方法”。【参考文献】

HotSpot为每个方法准备了两类计数器&#xff1a;方法调用计数器&#xff08;Invocation Counter&#xff09;和回边计数器&#xff08;Back Edge Counter&#xff0c;“回边”的意思 就是指在循环边界往回跳转&#xff09;。

方法调用计数器统计的是一段时间之内方法被调用的次数。当超过一定的时间限度&#xff0c;如果方法的调用次数仍然不足以让它提交给即时编译器编译&#xff0c;那该方法的调用计数器就会被减少一半&#xff0c;这个过程被称为方法调用计数器 热度的衰减&#xff08;Counter Decay&#xff09;&#xff0c;而这段时间就称为此方法统计的半衰周期&#xff08;Counter Half Life Time&#xff09;&#xff0c; 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的&#xff0c;可以使用虚拟机参数-XX&#xff1a;- UseCounterDecay来关闭热度衰减&#xff0c;让方法计数器统计方法调用的绝对次数&#xff0c;这样只要系统运行时间足够长&#xff0c;程序中绝大部分方法都会被编译成本地代码。


方法调用计数器触发即时编译

回边计数器&#xff0c;它的作用是统计一个方法中循环体代码执行的次数&#xff0c;在字节码中遇到控制流向后跳转的指令就称为“回边&#xff08;Back Edge&#xff09;”&#xff0c;很显然建立回边计数器统计的目的是为了触发栈上的替换编译。【参考文献】

当解释器遇到一条回边指令时&#xff0c;会先查找将要执行的代码片段是否有已经编译好的版本&#xff0c;如果有 的话&#xff0c;它将会优先执行已编译的代码&#xff0c;否则就把回边计数器的值加一&#xff0c;然后判断方法调用计数器与回 边计数器值之和是否超过回边计数器的阈值。当超过阈值的时候&#xff0c;将会提交一个栈上替换编译请求&#xff0c; 并且把回边计数器的值稍微降低一些&#xff0c;以便继续在解释器中执行循环&#xff0c;等待编译器输出编译结果

在这里插入图片描述
回边计数器触发即时编译


编译过程

在默认条件下&#xff0c;无论是方法调用产生的标准编译请求&#xff0c;还是栈上替换编译请求&#xff0c;虚拟机在编译器 还未完成编译之前&#xff0c;都仍然将按照解释方式继续执行代码&#xff0c;而编译动作则在后台的编译线程中进行。 用户可以通过参数-XX&#xff1a;-BackgroundCompilation来禁止后台编译&#xff0c;后台编译被禁止后&#xff0c;当达到触发即 时编译的条件时&#xff0c;执行线程向虚拟机提交编译请求以后将会一直阻塞等待&#xff0c;直到编译过程完成再开始执行编译器输出的本地代码。

Javac这类将Java代码转变为字节码的编译器称作“前 端编译器”&#xff0c;是因为它只完成了从程序到抽象语法树或中间字节码的生成&#xff0c;Java虚拟机内部的“后端编译器”来完成代码优化以及从字节码生成本地机器码的过程&#xff0c;即即时编译器&#xff0c;这个后端编译器的编译速度及编译结果质量高低&#xff0c;是衡量Java虚拟 机性能最重要的一个指标。【参考文献】

最新2021整理收集的一些高频面试题&#xff08;都整理成文档&#xff09;&#xff0c;有很多干货&#xff0c;包含mysql&#xff0c;netty&#xff0c;spring&#xff0c;线程&#xff0c;spring cloud、jvm、源码、算法等详细讲解&#xff0c;也有详细的学习规划图&#xff0c;面试题整理等&#xff0c;需要获取这些内容的朋友请加Q君样&#xff1a;756584822

在这里插入图片描述






推荐阅读
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Oracle seg,V$TEMPSEG_USAGE与Oracle排序的关系及使用方法
    本文介绍了Oracle seg,V$TEMPSEG_USAGE与Oracle排序之间的关系,V$TEMPSEG_USAGE是V_$SORT_USAGE的同义词,通过查询dba_objects和dba_synonyms视图可以了解到它们的详细信息。同时,还探讨了V$TEMPSEG_USAGE的使用方法。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
author-avatar
手机用户2502937657
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有