【JVM】详解执行引擎子系统、JIT即时编译原理

一、背景

执行引擎子系统是JVM的重要组成部分之一,JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。

但虚拟机与物理机最大的不同在于:物理机的执行引擎是直接建立在处理器、高速缓存、平台指令集与操作系统层面上的,物理机的执行引擎可以直接调用各处资源对代码进行直接执行,而虚拟机则是建立在软件层面上的平台,它的执行引擎则是负责解释编译执行自身定义的指令集代码。

同时,也正因Java设计出了JVM虚拟机的结构,从而才使得Java可以不受物理平台限制,能够真正实现“一次编译,到处执行”的理念。

二、机器码、指令集与汇编语言、高级语言的关系

在准备对JVM的执行引擎进行分析之前,首先得搞明白机器码、指令集、汇编语言以及高级语言之间的关系,只有你搞清楚这几者之间的关系后才能更好的弄懂JVM的执行引擎原理。

1.1、机器码

机器码也被称为机器指令码,也就是指各种由二进制编码方式表示的指令(011101、11110等),最开始的程序员就是通过这种方式编写程序,用这种方式编写出的代码可以直接被CPU读取执行,因为最贴近硬件机器,所以也是执行速度最快的指令。但因为这种指令和CPU之间是紧紧相关的,所以不同种类的CPU对应的机械指令也不同。同时,机械指令都是由二进制数字组成的指令,对于人来说,实在太过繁杂、难以理解且不容易记忆,容易出错,最终指令的方式代替了这种编码方式。

1.2、指令与指令集

由于机器码都是由0和1组成的指令代码,可读性实在太差,所以慢慢的推出了指令,用于替代机器码的编码方式。指令是指将机械码中特定的0和1组成的序列,简化为对应的指令,如INC、DEC、MOV等,从可读性上来说,对比之前的二进制序列组成的机器码要好上许多。但由于不同的硬件平台的组成架构也不同,所以往往在执行一个指令操作时,对应的机器码也不同,所以不同的硬件平台就算是同一个指令(如INC),对应的机器码也不同。

同时,正是因为不同的硬件平台支持的指令是有些稍许不同的,所以每个平台所支持的指令则被称为对应平台的指令集。比如X86架构平台对应的X86指令集、ARM架构平台对应的ARM指令集等。

1.3、汇编语言

前面虽然通过了指令和指令集的方式替代了之前由0和1序列组成的机器码,但指令的可读性相对来说还是比较差的,所以人们又发明了汇编语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)以及标号(Label)代替指令或操作数的地址。在不同的平台,汇编代码对应不同的指令集,但由于计算机只认机器码,所以通过汇编语言编写的程序必须还要经过汇编阶段,变为计算机可识别的机器指令码才可执行。

1.4、高级语言

为了使得开发人员编写程序更为简易一些,后面就涌现了各种高级语言,如Java、Python、Go、Rust等。高级语言对比之前的机器码、指令、汇编等方式,可读性更高,代码编写的难度更低。但通过高级语言编写出的程序,则需要先经过解释或编译过程,先翻译成汇编指令,然后再经过汇编过程,转换为计算机可识别的机器指令码才能执行。

JVM的主要任务是负责将javac编译后生成的字节码文件装载到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表和其他辅助信息,这些Java字节码指令是无法直接被OS识别的。那么一个Java程序可以在操作系统上跑起来的根本原因在于什么呢?答案是:依靠于JVM的执行引擎子系统。

三、初窥JVM执行引擎与源码编译原理

3.1、编译与解释

Java的执行引擎子系统的主要任务是将字节码指令解释/编译成对应平台上的本地机器指令,简单来说,JVM执行引擎是充当Java虚拟机与操作系统平台之间的“翻译官”的角色。

而目前主要的执行技术有:解释执行、静态编译、即时编译、自适应优化、芯片级直接执行,释义如下:

  • 解释执行:程序在运行过程中,只有当每次用到某处代码时,才会将某处代码转换为机器码交给计算机执行。

  • 静态编译:所谓的静态编译是指程序在启动前,先根据对应的硬件/平台,将所有代码全部编译成对应平台的机器码。

  • 即时编译:程序运行过程中,通过相关技术(如HotSpot中的热点探测)动态的探测出运行比较频繁的代码,然后在运行过程中,将这些执行比较频繁的代码转换机械码并存储下来,下次执行时则直接执行机器码。

  • 自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。

  • 芯片级直接执行:也就是直接编写机器码的方式,编写出的代码可以直接被CPU识别,读取后可以直接执行。

其实在Java刚诞生时,JDK1.0的时候,Java的定位是一门解释型语言,也就是将Java程序编写好之后,先通过javac将源码编译为字节码,再对生成的字节码进行逐行 解释执行 。但这样就导致了程序执行速度比较缓慢,启动速度也并不乐观,因为启动时需对于未编译的.java文件进行编译,而且编译之后生成的字节码指令也不能被计算机识别,还需要在执行时再经过一次 解释 后,才能变为计算机可识别的机器码指令,从而才能使得代码被机器执行。

经过如上分析,JDK1.0时的这种解释执行的缺点非常明显,Java为了做到“一次编译,到处运行”这个准则,将程序的综合性能大大拉低了,为什么呢?因为对比其他语言多了一个步骤。一般来说,一个Java程序想要运行,必须要经过 先编译,再解释 的过程才可以真正的执行。而我们此时再来看看其他语言的执行。

  • 纯编译型语言:在程序启动时,将编写好的源码全部编译为所处平台的机械码指令。

    • 特点:执行性能最佳,启动时间较长,移植性差,不同平台需要重新发包。

  • 纯解释型语言:在程序运行过程中,需要执行某处代码时,再将该代码解释为平台对应的机械码指令,然后交由计算机执行。

    • 特点:启动速度快,执行性能较差,移植性较好。

OK~,简单的看了一下解释型和编译型的语言特点之后,再回过头来想想1.0版本的Java,是不是发现Java因为虚拟机的存在,搞的不上不下的,卡在了中间。因为在Java程序运行时,既要编译源码,又要解释执行,所以最终导致执行性能一般,启动速度也一般。

再到后来,Java为了解决这个问题,在1.2的时候推出了一款后端编译器,也就是JIT即时编译器(后面分析),它可以支持在Java在执行过程中动态生成本地的机械码。现代的高性能JVM都是采用解释器与即使编译器共存的模式工作,所以Java也被称为“半解释半编译型语言”。

而本篇则会基于目前的HotSpot虚拟机对JVM的执行引擎进行分析,它的执行引擎中也采用解释器与即使编译器共存的模型工作,但这款虚拟机的执行模式采用的是 自适应优化 方案执行。

3.2、执行引擎工作过程

对于执行引擎而言,在《虚拟机规范》中曾提到了,要求所有厂商在实现时,输入输出都必须一致,也就是执行引擎接受的输入内容必须为字节码的二进制流数据,而输出的则必须为程序的执行结果。而执行引擎到底需要执行什么操作,完全是依赖于PC寄存器(程序计数器)的,每当执行引擎处理完一项指令操作后,程序计数器就需要更新下一条需要被执行的指令地址。

3.2.1、Java源码编译过程

在之前提及过,JVM只识别字节码文件,所以当编写好.java后缀的Java源码时,我们往往还需要通过javac这样的源码编译器(前端编译器),对Java代码进行编译生成.class后才能被JVM装载进内存。

编译是指将一种语言规范转化成另外一种语言规范,通常编译器都是将便于人理解的语言规范(编程语言)转化成机器容易理解的语言规范(由二进制序列组成的机械码)。比如C/C++或汇编语言都是将源代码直接编译成目标机器码。

javac作为Java语言的源码编译器,它编译的目的却不是为了针对于某个硬件平台进行编译的,而是为JVM进行编译,javac的任务就是将Java源代码转换为JVM可识别的字节码,也就是.java文件到.class文件的过程。对于怎么消除不同种类,不同平台之间的差异这个任务就交由JVM来处理,由JVM中的执行引擎来负责将字节码指令翻译成当前程序所在平台可识别的机械码指令。当程序运行过程中,调用某个方法时,就会将对应的字节码指令交由执行引擎处理。

总的来说,Java代码执行的过程会主要分为三个阶段,分别为:源码编译阶段、类加载阶段以及类代码(字节码)执行阶段,接着我们再来分析一下执行阶段的过程。

3.2.2、执行引擎执行过程

被加载进内存的字节码最终执行是由执行引擎来负责的,但JVM的执行引擎并不能真正的执行字节码指令,而是将字节码指令翻译成本地机械指令交由物理机的执行引擎来真正的执行的。

一般而言,在字节码被加载进内存之后,都会经过几个步骤才会被翻译成本地的机械指令执行,但有几个优化步骤却并不是必须的,如果不需要也可以在程序启动时通过JVM参数关闭。但综合而言,虽然优化的过程会耗费一些时间,但这样却能够大大的提升程序在执行时的速度,所以总归而言利大于弊。

下面来个简单的例子感受一下执行引擎执行的过程:

/* ------Java代码------ */
public int add(){
    int a = 3;
    int b = 2;
    int c = a + b;
    return c;
}

/* ------javap -c -v -p 查看到的字节码(省略描述方法的字节码)------ */
0: iconst_3 // 将3放入操作数栈顶
1: istore_1 // 写出操作数栈顶部元素,并将其放在局部变量表中索引为1的位置
2: iconst_2 // 将2放入操作数栈顶
3: istore_2 // 写出操作数栈顶部元素,并将其放在局部变量表中索引为2的位置
4: iload_1  // 从局部变量表中加载索引位置=1的数据值
5: iload_2  // 从局部变量表中加载索引位置=2的数据值
6: iadd     // 弹出操作栈顶的两个元素并进行 加 操作(3 + 2)
7: istore_3 // 将加之后的结果刷写到局部变量表中索引为3的位置
8:iload_3  // 从局部变量表中加载索引位置=3的数据值
8: ireturn  // 将加载的c返回

对于如上过程中,前四条分配指令就不分析了,重点分析一下后面的运算过程,也就是c=a+b这个过程,具体执行如下:

  • ①数据a从局部变量表经过总线传输到操作数栈

  • ②数据b从局部变量表经过总线传输到操作数栈

  • ③数据a从操作数栈经过总线传输给CPU

  • ④数据b从操作数栈经过总线传输给CPU

  • CPU计算完成后,将结果通过数据总线传输到操作数栈

  • ⑥运算结果从操作数栈经过总线传输到CPU

  • CPU将数据经过总线传输到局部变量表赋值给c

  • ⑧将计算后的结果从局部变量表索引为3的位置加载到操作数栈

  • ⑨最后使用ireturn指令将计算后的结果c返回给方法的调用者

如上便是栈式虚拟机的执行过程,其中所提到的局部变量表会在编译器确定长度,也就是等于一个this加上三个局部变量,长度最终为4。当程序执行到方法定义的那行代码时,局部变量表中会被依次填入数据:this、3、2,同时程序计数器会跟着代码的执行位置不断更新,当执行完add操作后,会将数据a+b的结果5再填入局部变量表。

四、详解JVM执行引擎子系统

简单的分析了一下Java代码的编译过程以及执行过程,同时在前面也提到了,Java是使用解释器+编译器共存的模式工作的,也就代表着JVM执行引擎子系统中,是包含了解释器和编译器的,如下图:

Java虚拟机的执行引擎子系统中包含两种执行器,分别为解释器和即时编译器。当执行引擎获取到由javac编译后的.class字节码文件后,在运行时是通过解释器(Interpreter)转换成最终的机械码执行。另外为了提升效率,JVM加入了一种名为 JIT即时编译 的技术,即时编译器的目的是为了避免一些经常执行的代码被解释执行,JIT会将整个函数编译为平台本地的机械码,从而在很大程度上提升了执行的效率。

4.1、解释器(Interpreter)

当Java程序运行时,在执行一个方法或某处代码时,会找到.class文件中对应的字节码,然后会根据定义的规范,对每条需执行的字节码指令逐行解释,将其翻译成平台对应对应的本地机械码执行。当一条字节码指令被解释执行完成后,紧接着会再根据PC寄存器(程序计数器)中记录的下一条需被执行指令,读取并再次进行解释执行操作。

在HotSpot虚拟机中,解释器主要由Interpreter模块和Code模块构成,Interpreter模块实现了解释执行的核心功能,Code模块主要用于管理解释器运行时生成的本地机械指令。

4.2、JIT即时编译器(Just In Time Compiler)

由于解释器实现简单,并且具备非常优异的跨平台性,所以现在的很多高级语言都采用解释器的方式执行,比如Python、Ruby、JavaScript等,但对于编译型语言,如C/C++、Go等语言来说,执行的性能肯定是差一筹的,而前面不止一次提到过:Java为了解决性能问题,所以采用了一种叫做JIT即时编译的技术,也就是直接将执行比较频繁的整个方法或代码块直接编译成本地机器码,然后以后执行这些方法或代码时,直接执行生成的机器码即可。

OK~,那么对于上述中 执行次数比较频繁的代码 判断基准又是什么呢?答案是:热点探测技术。

4.3、热点代码探测技术

HotSpot VM的名字就可以看出这是一款具备热点代码探测能力的虚拟机,所谓的热点代码也就是指调用次数比较多、执行比较频繁的代码,当某个方法的执行次数在一定时间内达到了规定的阈值,那么JIT则会对于该代码进行深度优化并将该方法直接编译成当前平台对应的机器码,以此提升Java程序执行时的性能。

一个被多次调用执行的方法或一处代码中循环次数比较多的循环体都可以被称为 热点代码 ,因此都可以通过JIT编译为本地机器指令。

4.3.1、栈上替换

纵观所有编程语言,类似于C/C++、GO等编译型语言,都属于静态编译型,也就是指在程序启动时就会将所有源代码编译为平台对应的机器码,但JVM中的JIT却属于动态编译器,因为对于热点代码的编译是发生在运行过程中的,所以这种方式也被称之为 栈上替换(On Stack Replacement),在有的地方也被称为OSR替换。

4.3.2、方法调用计数器

前面提到过:“一个被多次调用执行的方法或一处代码中循环次数比较多的循环体都可以被称为 热点代码”,那么一个方法究竟要被调用多少次或一个循环体到底要循环多少遍才可被称为热点代码呢?必然会存在一个阈值,而JIT又是如何判断一段代码的执行次数是否达到了这个阈值的呢?主要依赖于热点代码探测技术。

在HotSpotVM中,热点代码探测技术主要是基于计数器实现的。HotSpot中会为每个方法创建一个方法调用计数器(Invocation Counter),方法调用计数器主要用于统计方法被调用的次数。

方法调用计数器的阈值在Client模式下默认是1500次,在Server模式下默认是10000次,当一段代码的执行次数达到这个阈值则会触发JIT即时编译。当然,如果你对这些缺省(默认)的数值不满意,也可以通过JVM参数-XX :CompileThreshold来自己指定。

如上,当一个方法被调用执行时,会首先检查该方法是否已经被JIT编译过了,如果是的话,则直接执行上次编译后生成的本地机器码。反之,如果还没有编译,则先对方法调用计数器+1,然后判断计数器是否达到了规定的阈值,如果还未达到阈值标准则采用解释器的模式执行代码。如果达到了规定阈值则提交编译请求,由JIT负责后台编译,后台线程编译完成后会生成本地的机器码指令,这些指令会被放入Code Cache中缓存起来(热点代码缓存,存放在方法区/元数据空间中),当下次执行该方法时,直接从缓存中读取对应的机械码执行即可。

4.3.3、热度衰减

一般而言,如果以缺省参数启动Java程序,那么方法调用计数器统计的执行次数并不是绝对次数,而是一个相对的执行频率,也代表是指方法在一段时间内被执行的次数。当超过一定的时间,但计数器还是未达到编译阈值无法提交给JIT即时编译器编译时,那此时就会对计数器进行减半,这个过程被称为方法调用计数器的热度衰减(Counter Decay),而这段时间则被称为方法调用计数器的半衰周期(Counter Half Life Time)。

而发生热度衰减的动作是在虚拟机GC进行垃圾回收时顺带进行的,可以通过参数-XX:-UseCounterDecay关闭热度衰减,这样可以使得方法调用计数器的判断基准变为绝对调用次数,而不是以相对执行频率作为阈值判断的标准。不过如果关闭了热度衰减,就会导致一个Java程序只要在线上运行的时间足够长,程序中的方法必然绝大部分都会被编译为本地机器码。

  • 同时也可以通过-XX:CounterHalfLifeTime参数调整半衰周期的时间,单位为秒。

一般而言,如果项目规模不大,并且上线后很长一段时间不需要进行版本迭代的产品,都可以尝试把热度衰减关闭掉,这样可以使得Java程序在线上运行的时间越久,执行性能会更佳。只要线上运行的时间足够长,到后面可以与C编写的程序性能相差无几甚至超越(因为C/C++需要手动管理内存,管理内存是需要耗费时间的,但Java程序在执行程序时却不需要担心内存方面的问题,会有GC机制负责)。

4.4、JVM为何不移除解释器

在前面分析了JIT即时编译器,可以很直观的感受到,如果程序以纯JIT编译器的方式执行,性能方面绝对会超出解释器+编译器混合的模式,但为何虚拟机中至今也不移除解释器,还要用解释器来拖累Java程序的性能呢?

主要有两个原因,一个是为了保证Java的绝对跨平台性,另一个则是为了保证启动速度,考虑综合性能。

  • 保证绝对的跨平台性:如果将解释器从虚拟机中移除就代表着:每到一个不同的平台,比如从Windows迁移到Linux环境,那么JIT又要重新编译,生成对应平台的机器码指令才能让Java程序执行。但如果是解释器+JIT编译器混合的模式工作就不需要担心这个问题,因为前期可以直接由解释器将字节码指令翻译成当前所在的机械码执行,解释器会根据所在平台的不同,翻译出平台对应的机器码指令。这样从而使得Java具备更强的跨平台性。

  • 保证Java启动速度,考虑综合性能:因为如果移除了解释器模块,那么就代表着所有的字节码指令需要在启动时全部先编译为本地的机械码,这样才能使得Java程序能够正常执行。不过如果想在启动时将整个程序中所有的字节码指令全部编译为机器码指令,需要的时间开销是非常巨大的,如果把解释器从JVM中移除,那么会导致一些需要紧急上线的项目可能编译都需要等半天的时间。

综上所述,虚拟机移除解释器有移除后的隐患,当然,如果移除了也有移除之后的好处,比如JRockitVM中,就移除了解释器模块,从而使它获取了一个“史上最快”虚拟机的称号。

而HotSpot中采用的是解释器+JIT即时编译器混合的模式,这种模式的好处在于:在Java程序运行时,JVM可以快速启动,前期先由解释器发挥作用,不需要等到编译器把所有字节码指令编译完之后才执行,这样可以省去很大一部分的编译时间。后续随着程序在线上运行的时间越来越久,JIT发挥作用,慢慢的将一些程序中的热点代码替换为本地机器码运行,这样可以让程序的执行效率更高。同时,因为HotSpotVM中存在热度衰减的概念,所以当一段代码的热度下降时,JIT会取消对它的编译,重新更换为解释器执行的模式工作,所以HotSpot的这种执行模式也被成为“自适应优化”执行。

4.5、热冷机流量迁移注意事项

既然编译执行比解释执行的效率要高,那么就代表着程序如果处于编译执行的周期内,系统的吞吐量要比解释执行期间高很多。而Java现在默认的虚拟机HotSpot并不是一开始就是编译执行的,而是在运行过程中通过JIT即时编译器进行动态编译的。

所以现在可以得到一个简单的结论,Java程序的机器可以简单分为两种状态:

  • 热机:长时间在线上运行Java程序的机器,程序中很多代码都已经被JIT编译为了本地机器码指令。

  • 冷机:刚刚启动的Java程序的机器,所有代码还是处于解释执行的阶段。

从上面的分析中可以得知:机器在热机状态可以承受的流量负载会远远超出冷机状态。如果程序以热机状态切换流量到冷机状态的机器时,可能会导致冷机状态的服务器因无法承载流量而假死。

在开发过程中遇到这样的问题,某个服务因为要扩容,原本按照之前的集群规模计算,再扩容1/4之一左右的机器是可以承载新的流量的,但后面启动之后出现了问题,新启动的机器网关那边分配转发流量之后,立马就宕机了,最开始因为是第一次碰到这样的问题,以为是机器或者程序中代码的问题,最后排查发现都没问题,后来尝试将扩容的机器数量从原本计划的1/4增加到1/3之后,流量平滑的被迁移到了新的机器,没有再出现宕机的故障。

从上述这个案例中可以得知,如果直接将热机状态的流量迁移到冷机状态的机器是不可行的,所以一般在计划扩容时,想要流量平滑的切换到新的机器,一般有软硬件两种层面的解决方案,如下:

  • 第一种方案是和上述案例中一样,采用更多的机器承载热机状态过来的流量,等后续这些刚启动的冷机变成热机状态了,可以再把多余的机器停掉。

  • 第二种方案则是网关这边控制流量,先将一部分流量转发给刚启动的冷机,让刚启动的冷机先做预热,等运行一段时间之后再将原本计划的所有流量迁移到这些机器。

五、全面剖析JIT即时编译器

在JVM运行过程中采用的解释器+编译器混合执行的模式,一般是指JIT编译器,在Java中对于静态编译器的应用还是比较少的。在HotSpot虚拟机中内嵌着两个JIT即时编译器,分别为Client CompilerServer Compiler,也就是通常所说的C1和C2编译器,JVM在64位的系统中默认采用的C2编译器,也就是Server Compiler编译器。不过同样的,在程序启动的时候也可以通过参数显式指定运行时到底采用哪种编译器,如下:

  • -client:指定JVM运行时采用C1编译器。

    • C1编译器会对字节码进行简单和可靠的优化,耗时比较短,追求编译速度。

  • -server:指定JVM运行时采用C2编译器。

    • C2编译器会对字节码进行激进优化,耗时比较长,追求编译后的执行性能。

两种编译器因为追求的方向不同,所以在优化时的过程也存在差异,下面来简单分析一下C1和C2编译器。

5.1、C1编译器(Client Compiler)

C1编译器主要追求稳定和编译速度,属于保守派,C1中常见的优化方案有几种:公共子表达式消除、方法内联、去虚拟化以及冗余消除等。

  • 公共子表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那E的这次出现就成公共子表达式,可以用原先的表达式进行消除,直接使用上次的计算结果,无需再次计算。

  • 方法内联:将引用的方法代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。

  • 去虚拟化:对唯一的实现类进行内联。

  • 冗余消除:通过对字节码指令进行流分析,将一些运行过程中不会执行的代码消除。

    • 空检测消除:将显式调用的NullCheck(空指针判断)擦除,改成ImplicitNullCheck异常信号机制处理。

    • 自动装箱消除:对于一些不必要的装箱操作会被消除,比如刚装箱的数据又在后面立马被拆箱,这种无用操作就会被消除。

    • 安全点消除:对于线程无法抵达或不会停留的安全点会进行消除。

    • 反射消除:对于一些可以正常访问无需通过反射机制获取的数据,会被改为直接访问,消除反射操作。

5.2、C2编译器(Server Compiler)

C2编译器则主要是追求编译后的执行性能,属于激进派,C2编译器建立在C1编译器的基础优化之上,除开使用了C1中的优化手段之外,还有几种基于逃逸分析的激进优化手段:标量替换、栈上分配以及同步消除等。

  • 逃逸分析:逃逸分析是建立在方法为单位之上的,判断变量作用域是否存在于其他栈帧或者线程中,如果一个成员在方法体中产生,但是直至方法结束也没有走出方法体的作用域,那么该成员就可以被理解为未逃逸。反之,如果一个成员在方法最后被return出去了或在方法体的逻辑中被赋值给了外部成员,那么则代表着该成员逃逸了,判断逃逸的方法被称为逃逸分析。

    • 也可以换个说法,建立在线程的角度来看:如果一条线程中的对象无法被另一条线程访问到,就代表该对象未逃逸。

    • 逃逸的作用域:

      • ①栈帧逃逸:当前方法内定义了一个局部变量逃出了当前方法/栈帧。

      • ②线程逃逸:当前方法内定义了一个局部变量逃出了当前线程能够被其他线程访问。

    • 逃逸类型:

      • 全局变量赋值逃逸:当前对象被赋值给类属性、静态属性

      • 参数赋值逃逸:当前对象被当作参数传递给另一个方法

      • 方法返回值逃逸:当前对象被当做返回值return

  • 标量替换:建立在逃逸分析的基础上使用基本量标量代替对象这种聚合量。

    • 标量:reference与八大基本数据类型就是典型的标量,泛指不可再拆解的数据。

    • 好处:

      • ①能够节省堆内存,因为进行标量替换之后的对象可以在栈上进行内存分配。

      • ②相对运行而言省去了去堆中查找对象引用的过程,速度会更快一些。

      • ③因为是分配在栈上,所以会随着方法结束和线程栈的弹出自动销毁,不需要GC的介入。

  • 栈上分配:对于未逃逸的对象使用标量替换进行拆解,然后将拆解后的标量分配在局部变量表中,从而减少实例对象的产生,减少堆内存的使用以及GC次数。

    • 决定一个对象能否在栈上分配的因素(两个都必须满足):

      • ①对象能够通过标量替换分解成一个个标量。

      • ②对象在栈帧级作用域不可逃逸。

  • 同步消除:在出现synchronized嵌套的情况下,如一个同步方法中调用另一个同步方法,那么第二个同步方法的synchronized锁会被消除,因为第二个方法只有获取到了第一个锁的线程才能访问,不存在线程并发安全问题。

    • 决定能否同步消除(满足一个即可):

      • ①当前对象被分配在栈上。

      • ②当前对象的无法逃出线程作用域。

  • 空检查剪支:经过流分析后,对于一些不会执行的Null分支判断会直接剪掉

    • 如一个参数在外部方法传递前已经做了非空检测了,但在内部方法中依旧又做了一次非空判断,那么对于内部的这个非空判断会被直接剪除掉。

逃逸的作用域:①栈帧逃逸:当前方法内定义了一个局部变量逃出了当前方法/栈帧。 ②线程逃逸:当前方法内定义了一个局部变量逃出了当前线程能够被其他线程访问。全局变量赋值逃逸:当前对象被赋值给类属性、静态属性参数赋值逃逸:当前对象被当作参数传递给另一个方法方法返回值逃逸:当前对象被当做返回值return

前面提到了,64位的JVM中都是默认使用C2编译器的,但实际上JDK1.6之后如果是64位的机器,默认情况下或显式指定了-server模式运行时,JVM会开启分层编译策略,也就是通过C1+C2相互协作共同处理编译任务。而分层编译大体的逻辑为:Java程序刚启动还处于冷机状态时,采用C1编译器进行简单优化,追求编译速度和稳定性,当JVM达到热机状态时,后面的编译请求则通过C2编译器进行全面激进优化,追求编译后执行时的性能和效率。

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小颜-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值