15个问题自查你真的了解java编译优化吗?
摘要:为什么C++的编译速度会比java慢很多?二者运行程序的速度差异在哪? 了解了java的早期和晚期过程,就能理解这个问题了。
本文分享自华为云社区《你真的了解java编译优化吗?15个问题考察自己是否理解》,作者:breakDraw 。
首先提出一个问题,为什么C++的编译速度会比java慢很多?二者运行程序的速度差异在哪? 了解了java的早期和晚期过程,就能理解这个问题了。
这里会提15个问题确认是否真的理解,如果完全没这方面的概念,则好好看一下文章末尾的“jvm编译优化笔记”章节。
早期编译过程
Q: java早期编译过程分为哪3步?
A:
- 词法语法解析、填充符号表
- 注解处理
- 语义分析与字节码生成。
Q: 上面的步骤中, 符号表是干吗的?
A:
符号表是符号地址和符号信息构成的表格。
- 用于后面阶段做语法检查时,从表里取出信息进行对比。
- 符号表是目标代码生成时的地址分配的依据
Q: 注解处理器做的什么事情?
A: 注解处理器会扫描抽象语法树中带注解的元素, 并进行语法树的更新。
重点就是他是基于语法树做更新。
更新之后我们会重新走回解析与填充的过程,重新处理。
Q: 上面的3个步骤中, 解语法糖是哪一步?
A:
是第三步,在生成字节码的时候才做的语法糖处理。
Q: 什么是解语法糖?大概有哪些?
A:
- 虚拟机本身不支持这种语法, 但是会在编译阶段 把这些语法糖转为 普通的语法结构。
- 包含自动装拆箱、 泛型强转应用。
Q: 生成字节码class文件的时候, final和非final的局部变量, 会有区别不?
A:
没有区别。
局部变量不会在常量池中持有符号引用, 所以不会有acesses_flasg信息。
** 因此final局部变量在运行期没有任何作用, 只会在编译期去校验。**
Q: a= 1 + 2会在什么阶段进行优化?
A: 会在早期编译过程的语义分析过程中,进行常量折叠, 变成a=3
同理, 字符串+号优化成stringBuilder.append()这个动作也是该阶段优化的。
Q: 类对象加载的过程有一堆顺序(具体见类初始化顺序, 这个顺序在字节码中体现的吗?还是运行的时候再判断顺序?
A:
字节码中体现的。
- 在字节码生成时, 编译器针对对象new的过程,会生成了一个<init>方法,里面写明了成员、构造方法的调用顺序。
- 类静态成员的调用顺序同理封装在<cinit>中。
晚期编译优化
Q:
早期编译优化和晚期编译优化的区别?
A:
- 早期编译优化, 是把 java文件转成字节码,转字节码的过程中做一些简单优化和语法糖处理。
- 晚期编译优化,是将字节码转机器码执行的过程中,结合一些信息进行动态优化,或者应用上很多的机器码优化措施。
Q: java程序运行的时候,是直接全部转成优化后的机器码再运行吗?
A:
错误。
- 当程序刚启动时,会先马上使用解释器发挥作用,这时候没做太多优化,直接解释执行。
- 在程序运行后, 编译器逐步发挥作用,把还没用到的代码逐步编译成机器码。
注意这里的编译器和之前提到的编译器的区别,一个是编译成字节码,另一个是编译成机器码。
Q: 有两种晚期优化编译器
- Client Compiler ——C1编译器
- Server Compiler——C2编译器
他们二者的区别是什么?
A:
- 速度和质量的区别:
C1编译器, 更高的编译速度,编译质量一般。
C2编译器, 更好的编译质量,但是速度慢。 - 优化特性的区别
C1编译器都是一些不需要运行期信息就能做优化的操作。
C2编译器则会根据解释器提供的监控信息,进行激进且动态的优化
Q: java中怎么区分用C1还是C2?
A:
关于这2种编译器的参数:
- -Xint参数: 强制使用解释模式
- -Xcomp参数: 强制使用编译模式( 但是如果编译无法进行时, 解释会介入)
- 选择编译模式时,有-client、-server还有MixedMode(混合模式)可以选择
混合模式中, JDK7引入了分层编译策略:
第0层: 解释执行。 不开启性能监控。
第1层: C1编译, 把字节码编译为本地代码, 进行一些简单优化, 加入性能监控
第2层: C2编译, 启动耗时较长的优化, 根据性能监控信息进行激进优化
Q: 分层优化中,如果正在运行,jvm是怎么知道需要对哪些代码做JIT或者OSR优化?
A:
- 被多次调用的方法。 会触发JIT编译(热点代码计数器)
- 被多次执行的循环体, 会触发OSR编译(栈上替换), 发生在方法执行过程中, 所以是在栈上编译并切换方法。(使用回边计数器)
Q: 哪些方法会在早期优化中做内联,哪些方法会在晚期优化中做内联?
A:
- 不能被继承重写的方法,比如私有、构造器、静态之类的方法,可以直接在早期优化中做内联优化。
- 其他会被抽象继承实现的方法在早期无法做内联,因为他不知道实际是用哪一段代码.
- 晚期优化中可以根据一些运行信息,判断是否总是只用某个子类方法跑,是的话做一下尝试内联,如果后面来了其他的子类就切回去。
Q: java数组一般都会自动做边界检查,不满足就抛异常。 什么情况下会优化掉这个自动检查?
A:
运行期,发现传入的参数放到数组中用的时候, 肯定不会超出边界,则会优化掉这个检查动作。
看完上面的,就可以给出C++和java编译和运行速度差距的原因了:
- java即时编译可能会影响用户体验,如果在运行中出现较大影响的延迟的话。
- java中虚方法比C++要多, 因为做各种内联分析消耗的检查和优化的就越多越大
- java中总是要做安全检查, C++中不做,出错了我就直接崩溃了越界了
- C++中内存释放让用户控制, 无需后台弄一个垃圾回收器总是去检查和操作
- java好处: 即时编译能够以运行期的性能监控进行优化,这个是C++无法做到的
jvm编译优化学习笔记
早期
编译过程大致分为3类:
- 解析与填充符号表
- 注解处理
- 分析与字节码生成
关键点:
- 词法语法解析是第一步,生成符号
- 注解处理是第二步
- 然后语法糖、字节码都是第三步的事情。
上述步骤的详细解释:
第一步:
-------词法分析:
就是代码转成token标记。
例如int a=b+2 转成 Int \a=\b+\2 这6个token。
-------语法分析(注意实际上只是生成一个语法树,还没做语法的校验):
根据生成的token,构造一个抽象语法树。
-------填充符号表:
生成一个符号地址和符号信息构成的表格。
(后面第三步的阶段会用于语义分析中的标注检查, 比如名字的使用是否和说明一致,也会用于产生中间代码)
符号表是目标代码生成时的地址分配的依据
第二步:
-------注解处理器:
注解处理器会扫描抽象语法树中带注解的元素, 并进行语法树的更新。
更新之后我们会重新走回解析与填充的过程,重新处理。
这个处理器是一种插件,我们可以自己不断往其中去添加。
注意,上面这2步只是简单去对源文件做转换, 还不涉及任何语法相关的规则。
第三步:
-------语义分析:
判断语法树是否正确。分为2种检查:
- 标注检查: 检查变量是否已被声明、 赋值、等式的数据类型是否匹配
标注检查中会进行常量折叠, 把a=1+2折叠成3
标注检查的范围比较小,不会有太多上下文依赖。 - 数据及控制流分析
对程序上下文逻辑更进一步验证
这里会涉及很多交互的上下文交互依赖
比如 带返回值的方法是否全路径都包含了返回、 受检异常是否被外部处理、局部变量使用前是否被赋值。
final 局部变量(或者final参数)和非final局部变量,生成的class文件没有区别。
因为局部变量不会在常量池中持有符号引用, 所以不会有acesses_flasg信息。
所以class文件不知道局部变量是不是final, 因此final局部对运行期没有任何影响, 只会在编译期去校验。
-------解语法糖:
虚拟机本身不支持这种语法, 但是会在编译阶段 把这些语法糖转为 普通的语法结构(换句话说做了把语法糖代码变成了普通代码, 例如自动装拆箱,可能就是转成了包装方法的特定调用)
-------字节码生成:
对象的初始化顺序, 实际上会在字节码生成阶段, 收敛到一个<init>方法中。 即init中控制了那些成员、以及构造方法的调用顺序
类初始化同理,也是收敛到一个 <cinit>中
PS: 注意,默认构造器是在填充符号表阶段完成的。
字符串的替换(+操作转成sb) 是在字节码阶段生成的。
完成了对语法树的遍历之后,会把最终的符号表交给ClassWRITE类,设计概念从一个字节码和文件
晚期
HotSpot中, 解释器与编译器共存。
当程序刚启动时,会先马上使用解释器发挥作用。
在程序运行后, 编译器逐步发挥作用,把还没用到的代码逐步编译。
内存资源比较少的情况下,可以用解释器来跑程序,减少编译生成的文件。
如果编译器的优化出现bug,可以通过“逆优化”回退到最初的解释器模式来运行
解释器Interperter
编译器
有两种编译器
- Client Compiler ——C1编译器, 更高的编译速度
- Server Compiler——C2编译器, 更好的编译质量
即选择了-client或者-server时会用到。 - 默认混合模式: 解释器和编译器共存, 即MixedMode。
关于这2种编译器的参数:
- -Xint参数: 强制使用解释模式
- -Xcomp参数: 强制使用编译模式( 但是如果编译无法进行时, 解释会介入)
混合模式中, 解释器需要收集性能信息,提供给编译阶段判断和优化, 这个性能信息有点浪费
因此JDK7引入了分层编译策略:
- 第0层: 解释执行。 不开启性能监控。
- 第1层: C1编译, 把字节码编译为本地代码, 进行一些简单优化, 加入性能监控
- 第2层: C2编译, 启动耗时较长的优化, 根据性能监控信息进行激进优化
CC和SC编译过程的区别:
- client Compiler 编译过程:
前端字节码-》 方法内联/常量传播(基础优化)-》 HIR(高级中间代码)-》 空值检查消除/范围检查消除
-》 后端把HIR转成LLR(低级中间代码)-》 线性扫描算法分配寄存器-》窥孔优化-》机器码生成-》本地代码生成
都是一些不需要运行期信息就能做优化的操作 - serverCompiler编译过程:
会执行所有的经典优化动作
会根据cc或者解释器提供的监控信息,进行激进的优化
寄存器分配器是一个全局图着色分配器
晚期优化的一些常见措施(即运行中才会做优化的步骤)
----热点代码
- 被多次调用的方法。 会触发JIT编译
- 被多次执行的循环体, 会触发OSR编译(栈上替换), 发生在方法执行过程中, 所以是在栈上编译并切换方法。
HotSpot 使用 计数器的热点探测法确定热点代码。
* 给每个方法建立方法计数器, 在一个周期中如果超过阈值, 就触发JIT编译,编译后替换方法入口。
* 如果一个周期内没超过,则计数器/2(半衰)
* 如果没有触发时, 都是用解释方式 按照字节码内容死板地运行。
该计数器的相关参数
-XX:-UserCounterDecay 关闭热度衰减
-XX: CounterHalfLifeTime 设置半衰期-XX:CompileThreshold 设置方法编译阈值
回边计数器就是计算循环次数的计数器
* 没有半衰
* 但是当触发OSR编译时,会把计数器降低,避免还在运行时重复触发。
* 会溢出, 并且会把方法计数器也调整到溢出。
* clint模式和server模式中, OSR的阈值计算公式不同, clint= CompileThredshold * osr比率, server= CompileThredshold * (osr比率 - 解释器监控比率)
—冗余访问消除:
如果已经拿到了 a.value, 该方法内a.value一定不会变的话, 那么后续用到时就不再从a中取value了
复写传播:
y=b.value
z=y
c = z + y
变成
y = b.value
y = y
c = y + y
无用代码消除:
去掉上面的Y=y
----公共子表达式消除
就是对一些比较长的计算公式做化简
a+(a+b)2
会优化成
a3+b*2
尽可能减少计算次数
—数组边界检查:
如果能确定某个for循环里的数组取值操作一定不会超出数组范围,那么在做[]取值操作时,不会做数组边界检查。
—隐式异常处理:
if(a == null) {
xxx
}
else{
throw Exception
}
优化成
try {
xxx
} catch(Exception e) {
throw e
}
----方法内联:
不能被继承重写的方法,比如私有、构造器、静态之类的方法,可以直接在早期优化中做内联优化。
而其他会被抽象继承实现的方法在编译器无法做内联,因为他不知道实际是用哪一段代码。
- final方法并不是非虚方法(为什么呢)
- 类型继承关系分析CHA: 如果发现虚方法,CHA会查一下当前虚拟机内该方法是否有多个实现, 如果发现只有这一种实现,那么就可以直接内联。
- 如果后续有其他的class动态加载进来后,该方法有多个实现了,并且被使用到了,那么就会抛弃已编译的内联代码,回退到解释状态执行。
- 内联缓存: 即使程序中发现该方法有多个实现, 依然对第一个使用的那个方法做内联,除非有其他重写方法被调用(即虽然你定义了,但是你很可能不用,所以我一直使用你的第一个方法,除非你真的用了多种重写方法去跑。
----逃逸分析:
分析new 出来的对象是否不会逃逸到方法外, 如果确认只在方法内使用,外部不会有人引用他, 那么就会做优化,比如:
* 不把new出来的对象放到堆,而是放到方法栈上,方法结束了对象直接消失。
* 不需要对这种对象做加锁、同步操作了
* 标量替换: 把这个对象里的最小基本类型成员拆出来作为局部变量使用。
----java和C++, 即时编译和静态编译的区别:
- 即时编译可能会影响用户体验,如果在运行中出现较大影响的延迟的话
java中虚方法比C++要多, 因为做各种内联分析消耗的检查和优化的就越多越大
3.
java中总是要做安全检查, C++中不做,出错了我就直接崩溃了越界了
4.
C++中内存释放让用户控制, 无需后台弄一个垃圾回收器总是去检查和操作
5.
java好处: 即时编译能够以运行期的性能监控进行优化,这个是C++无法做到的。
15个问题自查你真的了解java编译优化吗?的更多相关文章
- java编译优化
#java编译器对`String常量表达式`的优化: - 1.String+String 可以被编译器识别为常量表达 String a="ab" ; String b=" ...
- Java随谈(六)## 我们真的理解 Java 里的整型吗?
我们真的理解 Java 里的整型吗 整型是我们日常生活中最常用到的基础数据类型,看这篇文章之前,我想问: 我们真的像自己认为的那么理解 Java 内的整型吗? 也许看完本篇文章你就有自己的答案. C ...
- 你真的理解Java 注解吗?
你真的理解Java 注解吗? 1.什么是注解? 官方解释: Java 注解用于为 Java 代码提供元数据.作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的.Java ...
- 你真的了解java的lambda吗?- java lambda用法与源码分析
你真的了解java的lambda吗?- java lambda用法与源码分析 转载请注明来源:cmlanche.com 用法 示例:最普遍的一个例子,执行一个线程 new Thread(() -> ...
- 【转】10种简单的Java性能优化
10种简单的Java性能优化 2015/06/23 | 分类: 基础技术 | 14 条评论 | 标签: 性能优化 分享到: 本文由 ImportNew - 一直在路上 翻译自 jaxenter.欢迎加 ...
- Java 程序优化 (读书笔记)
--From : JAVA程序性能优化 (葛一鸣,清华大学出版社,2012/10第一版) 1. java性能调优概述 1.1 性能概述 程序性能: 执行速度,内存分配,启动时间, 负载承受能力. 性能 ...
- Java 性能优化之 String 篇
原文:http://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/ Java 性能优化之 String 篇 String 方法用于文本分析 ...
- Java性能优化_转载
一.避免在循环条件中使用复杂表达式 1.在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 2.不可使用多层循嵌套. 二.集合大 ...
- Java性能优化的50个细节(珍藏版)
原文地址:https://www.toutiao.com/i6595499804082569742/ 在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重 ...
随机推荐
- 如何在win10中Java中JDK的安装和path,classpath的环境配置
1,第一步,不用说肯定是去下一个java JDK了.目前最新版本的java JDK应该是JDK 7.0,这个就自己去百度一下了,好多网站都可以找到.2,第二步就是安装JDK虚拟机了,按照它里面的提示一 ...
- Spring Cloud Eureka 之常用配置解析
[原创内容,转载.引用请注明出处] 1. 配置项解析 1.1 通用配置 # 应用名称,将会显示在Eureka界面的应用名称列 spring.application.name=config-servic ...
- 谈谈raft fig8 —— 迷惑的提交条件和选举条件
谈谈raft fig8 -- 迷惑的提交条件和选举条件 前言 这篇文章的思路其实在两个月前就已经成型了,但由于实习太累了,一直没来得及写出来.大概一个月前在群里和群友争论fig8的一些问题时,发现很多 ...
- CVPR顶会论文爬取存入MySQL数据库(标题、摘要、作者、PDF链接和原地址)
main.py import pymysql import re import requests # 连接数据库函数 from bs4 import BeautifulSoup def insertC ...
- 【C++周报】第一期2021-8-1
[C++周报]第一期 2021-8-1 这一期我们来看这道题目:https://vijos.org/p/1058 这道题是一道非常好的模拟题.题目如下: 描述 我们用文本处理器来处理一个特殊的文本文件 ...
- umi request 请求资源库详解
umi-request: 网络请求库,基于fetch封装,兼具fetch 和 axios 的所有特点,具有缓存,超时,字符编码处理,错误处理等常用功能. 1 支持url 参数自动序列化. 2 post ...
- 学习PHP中Fileinfo扩展的使用
今天来学习的这个扩展其实现在也已经是标配的一个扩展了,为什么呢?因为 Laravel 框架在安装的时候它就是必须的一个扩展,没有打开它的话,连 Laravel 框架都是无法使用的. Fileinfo ...
- Nginx TP5环境配置
Apache默认支持Pathinfo模式 Nginx不支持 需要手动配置 Apache默认支持Pathinfo模式 Nginx不支持 需要手动配置 server { #配置监听端口 list ...
- TP5模型开启事务
和Db开启事务类似,Db是静态方法 $userObj = new UserModel(); $userObj->startTrans(); try { $userObj->data($da ...
- dede5.7 给栏目添加上缩略图
如我们一个栏目列表都用缩略图来表示,而不仅仅只是文字,如果没有这项功能会非常麻烦,网上有很多这方面的资料,但是都试过了有很多问题,自己研究一下,测试基本通过.新加字段 typeimg后台执行SQL: ...