jvm调优的6个参数,双十一高并发系统JVM调优实战
jvm调优的6个参数,双十一高并发系统JVM调优实战实现的功能:查看是否已经启动成功:192.168.31.133:8080/movie/query为了方便测试性能,我们将部署一个java web项目,这个项目本身和本套课程没有什么关系,仅仅用于测试。在资料中找到itcast-gc-demo-1.0.war,上传到linux服务器,进行部署安装。重新启动tomcat。
1、JVM调优实战1.1、环境准备测试环境需要1台Linux支持。
jdk使用的版本为jdk8,需要在linux服务器中安装jdk8:
1.1.1、安装tomcattomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了。
对于tomcat的优化,主要是从2个方面入手,一是,tomcat自身的配置,另一个是tomcat所运行的jvm虚拟机的调优。
1.1.3、部署web项目为了方便测试性能,我们将部署一个java web项目,这个项目本身和本套课程没有什么关系,仅仅用于测试。
在资料中找到itcast-gc-demo-1.0.war,上传到linux服务器,进行部署安装。
重新启动tomcat。
查看是否已经启动成功:192.168.31.133:8080/movie/query
1.1.4、测试项目实现的功能:
- 读取电影文件movies.dat,载入到内存
- 查询电影数据时,随机返回1w ~ 10W个数据
代码:
1.2、压力测试下面我们通过jmeter进行压力测试,先测得在初始状态下的并发量等信息,然后我们在对jvm做调优处理,再与初始状态测得的数据进行比较,看调好了还是调坏了。
1.2.1、添加gc日志相关参数重新启动tomcat。
1.2.2、创建测试用例 1.2.3、初始测试初始测试使用200的并发进行测试,每个线程执行100次,总数为20000次请求。
测试结果:
平均响应时间:3033ms
吞吐量:32.4/sec (每秒的请求量)
错误率:0.87%
测试过程中的cpu、内存等信息。
1.2.4、GC日志解析在初始测试中,将生成的gc日志上传到gceasy进行分析,我们将看到一些非常有价值的信息,对我们的后续调优有着重要的指导作用。
在jvm内存统计中可以看出,年轻代与老年代在高峰时,基本将可用空间都占满了,说明内存空间不足,需要调整内存大小。
关键指标中,吞吐量为98.694%,gc平均停顿时间为8.19ms,最大停顿时间为180ms。
可以看出,在初始状态时,吞吐量并不高,最大停顿时间较长,平均停顿时间表现不错。
在图表中可以看出,gc之后堆内存的使用基本是在60m ~ 80m之间。
在图表中可以看出,gc之前的堆内存的使用在高峰时,将占用到80m ~ 120m之间。
在gc持续时间统计中,可以看出full gc的时间要远高于younggc的时间,在调优时应当尽量的减少full gc。
在清理垃圾的统计中,可以看出gc清理的垃圾基本维持在40m左右,最多的一次是发生在full gc,可以推断此次是内存即将耗尽,发生了fullgc,释放了大量的内存空间,这也是在前面年轻代对象有部分进入到老年代,在此次fullgc时被清理了。
从年轻代的gc情况来看,gc之前与gc之后差较大,说明垃圾对象在年轻代被清理的比较多,就是说临时性的对象居多。
从老年代的gc情况来看,gc之前与之后的差并不大,说明老年代的垃圾对象并不是很多。
Meta Space空间充足,基本没有变化,占用空间40m左右。
从该图中看出,晋升到老年代的对象与可以分配对象相比,非常的少,也说明了上面我们看到的,对象主要集中在young区。
在GC统计中,可以看出:
- Minor GC清理掉的垃圾对象合计24.13gb,说明产生的临时对象非常的多
- Minor GC的执行间隔为636ms,说明发生gc的行为是比较频繁的
- Full GC发生了10次,较为频繁
- Full GC的平均持续时间为90ms,时间较长
- GC的暂停次数为690次,暂停次数将影响到服务的响应时间
在对象的统计中,可以看出对象的平均生成率:57.85m/s,平均的晋升率:610kb/s。
在GC原因统计中可以看出,大部分发生gc的原因都为分配失败,也就说内存不足导致;需要说明的是虽然发生了4次Metadata GC,并不是Metaspace不足导致,前面我们看到Metaspace空间充足,而是该gc发生在最开始时间,说明初始的Metaspace不足,导致了Metaspace扩容,并发生了GC,可以适当调整Metaspace初始大小以减少Metadata GC次数。
1.3、调优一:调整内存在jvm调优中,调整内存大小是调优手段中最为基本的一种手段,但是需要注意的是,内存的调整并不是简单的加大内存,而是需要结合业务特性、gc类型等内容进行调整。
对于我们目前测试的应用而言,属于及时响应、低延迟的应用,这样的应用在jvm堆内存中,对象的存活时间较短,所以应该将年轻代的内存调大些。
测试结果:
可以看到内存基本够用,从结果来看,可以进一步扩大年轻代的大小。
吞吐量与停顿时间都比较理想。
可以看出,没有发生Full GC,Minor GC的次数明显下降。情况有了很大的改善。
GC原因中,也只有一种原因,这是属于正常的情况。
总体来讲,通过调整内存大小,对于服务的性能有了显著的提升。
下面尝试下,加大并发到400,看下表现怎么样?
可以看到,在400并发的情况下,吞吐量为395,平均响应时间为945ms,相比较200并发下的结果,稍微差了一下,整体还算ok。
可以按照这个方法,测试500、800、1000或者更高并发。
1.4、调优二:更换G1收集器选择性能更优的垃圾收集器也是调优的手段之一,在jdk8中,使用率最高的当属G1收集器了,下面我们就尝试切换成G1收集器,来看下它的表现。
测试结果:
这个结果要比同等内存大小的ParallelGC性能稍好一些,但是提升并不明显。
自分配的内存区域大小,也基本符合需求。
在吞吐量指标中,有所下降,但是停顿时间都有所减少。
总结:
- 更换G1垃圾收集器后,其性能有所提升,但是并不明显,原因有两个:
- 调优一中并未出现明显的问题,所以相对比,不是很明显
- G1垃圾收集器适合大内存低延迟的场景,比如设置6G、8G内存的场景下保持低延迟
下面我们将垃圾收集器换成ZGC,需要注意的时,jdk需要切换到jdk11版本。
测试结果:
测试结果中的吞吐量与平均响应时间要比G1稍差一些,差距并不太大。
在GC方面的表现,无论是吞吐量还是停顿时间均有不俗的表现。
综合起来看的话,ZGC的表现还是很不错的,如果给其设置大内存,依然可以得到较短的停顿时间。
1.6、调优建议对于JVM的调优,给出大家几条建议:
- 生产环境的JVM一定要进行参数设定,不能全部默认上生产。
- 对于参数的设定,不能拍脑袋,需要通过实际并发情况或压力测试得出结论。
- 对于内存中对象临时存在居多的情况,将年轻代调大一些。如果是G1或ZGC,不需要设定。
- 仔细分析gceasy给出的报告,从中分析原因,找出问题。
- 对于低延迟的应用建议使用G1或ZGC垃圾收集器。
- 不要将焦点全部聚焦jvm参数上,影响性能的因素有很多,比如:操作系统、tomcat本身的参数等。
PerfMa提供了JVM参数分析、线程分析、堆内存分析功能,界面美观,功能强大,我们在做jvm调优时,可以作为一个辅助工具。官网:perfma/
1.7.1、XXFoxXXFox是Java虚拟机参数分析工具,可以对JVM参数进行查询、检查、优化等。
参数优化:
参数生成:
1.7.2、XSheepdogXSheepdog是Java线程Dump分析工具,通过jstack生成线程的dump文件,进行分析。
1.7.3、XElephantXElephant是Java内存Dump分析工具。
获取内存 Dump 文件的命令:
生成 Dump 文件后,就可以通过 控制台 > Java内存Dump分析 ,选择合适的途径上传到服务器进行分析。
2、百亿级流量电商大促高并发系统下JVM如何调优?结合前面我们所学习到知识,我们来看个实际的案例,百亿流量电商大促高并发,如何调优?
面对这个问题,首先不要被标题吓到,需要我们冷静、仔细的分析,才能解决这个问题。
- 百亿流量的电商网站,一般日活跃用户可能在1亿左右,大促期间的参与人较平常会增加很多,比如5亿左右。
- 电商系统的业务线非常多,大促期间压力最大的属订单、支付业务,我们聚焦到订单系统。
- 假设,部署了订单系统的微服务数量为100,每台机器4核8G内存。
- 一般情况下,电商网站的付费转化率为10%左右,所以,1亿的活跃用户会产生1000w订单。
- 一天24小时,以12小时下单时间计算,每小时产生83w订单,每秒钟产生230左右个订单。
- 平均到100台机器,每台机器2~3个订单/秒,可以说毫无压力。
- 大促期间,转化量会提升,假设提升到50%左右,所以,5亿活跃用户会产生2.5亿订单。
- 大促期间的用户下单与平常的有所不同,这里我们假设,一天下单时间以20小时计算,前1小时,下单30%,其它时间段产生70%的订单。
- 前一小时产生30%订单,也就是7千500万订单,每秒钟将会产生20833个订单,分摊到100台服务器,每台服务器208个/秒订单的产生。
- 其它时间段产生70%的订单,也就是1亿7千5百万个订单,每小时产生920万订单,每秒产生2558个订单,分摊到100台机器,每天机器26个/秒订单的产生。
- 通过上面的分析,一般情况和大促的非集中时间段,产生的订单量较少,每台机器的压力并不大,所以我们只需要关注大促开始的1小时的压力即可。
- 假设每个订单对象的大小为1KB,考虑到每个订单的产生会有复杂的业务流程,我们将订单对象大小扩大100倍,也就是,每产生一个订单对象,会占用100KB的内存空间。也就是说,在大促期间,前一小时,每秒钟占用的内存空间为:100KB * 208 = 20800KB 也就是 20M左右空间。
- 每台机器的内存是8G,除去操作系统、监控等内存开销,配置到jvm虚拟机的堆内存空间为6G。
- 考虑到订单对象在内存中创建后,会很快被持久化到数据库,随后就会GC回收,所以属于短暂性存活对象,尽可能的让其待在年轻代,避免过多的对象进入老年代,从而导致Full GC。
- 在JDK8中,建议使用G1垃圾收集器,下面是给出的jvm主要参数建议:
了解Java的类文件结构,可以帮助我们进一步的对jvm的理解,在做jvm调优时,可以做到知其所以然。
Java程序最终是转换成Class文件执行在虚拟机上的,那么class文件是什么样的的结构,虚拟机又是如何处理去执行class文件里面的内容呢?
3.1、MyDemo上面的java文件编译得到的class文件,class文件是一个16进制字节码的二进制文件,我们通过winhex软件打开如下:
3.2、字节码要想读懂上面的字节码内容,就需要通过字节码表来查询对应的含义了。
3.2.1、字节码表需要说明的是,class文件只有两种数据类型: 无符号数 和 表 。
- 无符号数
- 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。
- 其中无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节。
- 表
- 表是由多个无符号数或其他表构成的复合数据结构。
- 所有的表都以“_info”结尾,由于表没有固定长度,所以通常会在其前面加上个数说明。
在class文件中,前4个字节就是魔数,如下:
魔数是用来区分文件类型的一种标识,0XCAFEBABE (咖啡豆/咖啡宝贝)表示就是class文件,之所以不使用文件后缀名区别就是后缀名太容易被改了,不安全。
3.2.3、版本号魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号,后2个字节表示主版本号。
前面两个字节是0x0000 也就是其值为0; 后面两个字节是0x0034 也就是其值为52; 所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0。
3.3、常量池在版本号的后面就是常量池了。
3.3.1、常量池容量计数值由于常量池的数量不固定,所以需要通过2个字节来记录常量池的大小,其值为0X002E,用十进制表示就是46,需要注意的是,常量池中只有45个常量。
与Java中语言习惯不同,这个容量计数是从1而不是0开始的,在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
3.3.2、常量类型和结构常量池中的每一项都是一个表,其项目类型共有14种,如下表格所示:
这14种类型的结构各不相同,如下表格所示:
从上面的表格可以看到,虽然每一项的结构都各不相同,但是他们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。
3.3.3、第一个常量可以看到,第一个常量的值为10,对应到表中的标志位,找到常量为 CONSTANT_Methodref_info (类中方法的符号引用),它的结构为:
后面的4个字节都是它的内容,记录着2个索引值:
第一个索引值为:0X0009,也就是9,指向常量池中第9项的索引。
第二个索引值为:0X001E,也就是30,指向常量池中第30项的索引。
3.3.4、第二个常量可以看到,第一个常量的值为9,对应到表中的标志位,找到常量为 CONSTANT_Fieldref_info(字段的符号引用),它的结构为:
后面的4个字节都是它的内容,记录着2个索引值:
第一个索引值为:0X0004,也就是4,指向常量池中第4项的索引。
第二个索引值为:0X001F,也就是31,指向常量池中第31项的索引。
3.3.5、javap 编译字节码通过javap命令就可以将class文件转化为可读的字节码指令。
生成的字节码内容如下,可以看到与我们前面分析的是一致的。
内容大致分为4个部分:
第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。
第二部分:显示了该类中所涉及到常量池,共45个常量。
第三部分:显示该类的构造器,编译器自动插入的。
第四部分:显示了sum、main方的信息。(这个是需要我们重点关注的)
3.4、描述符3.4.1、字段描述符官网:docs.oracle/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
3.4.2、方法描述符
官网:docs.oracle/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
示例:
The method descriptor for the method:
3.5、实例:字符串拼接字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种:
- 号拼接: str "456"
- StringBuilder拼接
- StringBuffer拼接
StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。
那么,问题来了,StringBuilder和“ ”号拼接,哪个效率高呢?接下来我们通过字节码的方式进行探究。
首先,编写个示例:
通过字节码的方式进行查看:
从解字节码中可以看出,m1()方法源码中是使用 号拼接,但是在字节码中也被编译成了StringBuilder方式。
所以,可以得出结论,字符串拼接, 号和StringBuilder是相等的,效率一样。
小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。
4、类加载机制Java源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行。虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
4.1、类加载时机一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如下图所示。
上图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令;
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候;
- 当初始化一个类的时候,发现其父类还没有进行初始化的时候,需要先触发其父类的初始化;
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invokethodHandle 实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》中使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
比如如下几种场景就是被动引用:
- 通过子类引用父类的静态字段,不会导致子类的初始化;
- 通过数组定义来引用类,不会触发此类的初始化;
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;
在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段大致上会完成下面 4 个阶段的检验动作:
- 文件格式验证
- 第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能够被当前版本的虚拟机处理。验证点主要包括:
- 是否以魔数 0xCAFEBABE 开头;
- 主、次版本号是否在当前虚拟机处理范围之内;
- 常量池的常量中是否有不被支持的常量类型;
- Class 文件中各个部分及文件本身是否有被删除的或者附加的其它信息等等。
- 元数据验证
- 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,这个阶段的验证点包括:
- 这个类是否有父类;
- 这个类的父类是否继承了不允许被继承的类;
- 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法;
- 类中的字段、方法是否与父类产生矛盾等等。
- 字节码验证
- 第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证
- 最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段--解析阶段中发生。
- 符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
4.2.4、解析解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
4.2.5、初始化类初始化阶段是类加载过程中的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。
到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
4.3、类加载器Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
4.3.1、类与类加载器类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
4.3.2、双亲委派模型从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C 来实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由 Java 来实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader 。
从 Java 开发者的角度来看,类加载器可以划分为:
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在 <java_home>\lib 目录中的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用 null 代替即可;
- 扩展类加载器(Extension ClassLoader):这个类加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 <java_home>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;
- 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$App-ClassLoader 实现。 getSystemClassLoader() 方法返回的就是这个类加载器,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这 3 种类加载器互相配合进行加载的,在必要时还可以自己定义类加载器。它们的关系如下图所示:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
双亲委派模型的工作过程是:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类
- 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此
- 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
这样做的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object,它放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器来加载,因此Object 类在程序的各种类加载器环境中都是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基本的行为也就无法保证了。
双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中:
5、编译优化如何使我们编写的代码可以更加高效的执行,除了源码层面的优化外,虚拟机中的编译器也是在背后默默的为代码做着优化工作,下面我们就来了解下编译器做的一些优化行为。
5.1、前端编译器前端编译器就是将*.java文件编译成*.class文件的过程。
前端编译器能够做的优化是非常有限的,主要的优化工作在后端编译器完成。
5.1.1、javac编译过程javac的编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示。
上图是Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类主要执行流程,页反应出javac的执行流程。
5.2、后端编译与优化后端编译器是指把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码的过程。
后端编译器一般分为即时编译器与提前编译器:
5.2.1、编译器的优化编译器的目标虽然是做由程序代码翻译为本地机器码的工作,但其实难点并不在于能不能成功翻译出机器码,输出代码优化质量的高低才是决定编译器优秀与否的关键。
OpenJDK的官方Wiki上,HotSpot虚拟机设计团队列出了一个相对比较全面的、即时编译器中采用的优化技术列表,地址:wiki.openjdk.java/display/HotSpot/PerformanceTacticIndex
一般来讲,最具代表性的优化技术有四种,分别是:
- 方法内联
- -逃逸分析
- 公共子表达式消除
- 数组边界检查消除
方法内联,是指JVM在运行时将调用次数达到一定阈值的方法调用替换为方法体本身,从而消除调用成本,并为接下来进一步的代码性能优化提供基础,是JVM的一个重要优化手段之一。
举个例子:
5.2.3、逃逸分析逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是:
- 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
- 甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
- 从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低,则可能为这个对象实例采取不同程度的优化。
示例(伪代码):
5.2.4、公共子表达式消除公共子表达式消除是一项非常经典的、普遍应用于各种编译器的优化技术。
它的含义是:如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。
对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替E。
如果这种优化仅限于程序基本块内,便可称为局部公共子表达式消除,如果这种优化的范围涵盖了多个基本块,那就称为全局公共子表达式消除。
5.2.5、数组边界检查消除数组边界检查消除是即时编译器中的一项语言相关的经典优化技术。
如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候系统将会自动进行上下界的范围检查,即i必须满足“i>=0&&i<foo.length”的访问条件,否则将抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException。
对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这必定是一种性能负担。
对于编译器而言,根据数据流分析来确定是否越界,如果没有越界,那么执行时就不需要再判断了。
如果在循环中,本身就是通过循环变量来控制对数组的访问,执行时也就不用再判断了,这样就可以把整个数组的上下界检查消除掉,这可以节省很多次的条件判断操作。