在Java编译系统中,一个Java源代码文件在转换为计算机可执行的机器指令的过程中,需要经过两个阶段的编译。第一阶段是将.java文件转换为.class文件。编译的第二阶段是将.class转换为机器指令的过程。第一段编译是javac命令。在第二个编译阶段,JVM通过解释将字节码翻译成相应的机器指令,一条条读取,一条条解释翻译。显然,经过解释执行后,其执行速度必然会比可执行的二进制字节码程序慢很多。这就是传统的JVM解释器(Interpreter)的作用。为了解决这个效率问题,引入了JIT(即时编译)技术。JIT技术引入后,Java程序仍然是通过解释器来解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会认为它是“热点代码”。然后JIT会将一些“热点代码”翻译成本地机器相关的机器码,进行优化,然后将翻译后的机器码缓存起来,以备下次使用。因为JIT编译和热点检测的内容,我在Java编译原理深入分析中已经介绍过,这里不再赘述。本文主要介绍JIT中的优化。JIT优化最重要的方面之一是逃逸分析。逃逸分析逃逸分析的概念可以参考对象不一定在堆上分配内存的文章。这里简单回顾一下:逃逸分析的基本行为是分析对象的动态作用域:当一个对象定义在一个方法中时,它可能会被外部方法引用,比如作为调用参数传递到其他地方,这称为方法逃逸。例如下面的代码:sb.append(s1);sb.append(s2);returnsb.toString();}***段代码中的sb转义,但是第二段代码中的sb没有转义。使用逃逸分析,编译器可以优化代码如下:1.省略了同步。如果发现一个对象只能从一个线程访问,则对该对象的操作可能不会同步。2.将堆分配转换为栈分配。如果一个对象是在子程序中分配的,如果指向该对象的指针永远不会逃逸,则该对象可能是堆栈分配而不是堆分配的候选对象。3.分离对象或标量替换。有些对象可能在不作为连续内存结构存在的情况下被访问,因此对象的部分(或全部)可能不存储在内存中,而是存储在CPU寄存器中。Java代码运行时,可以通过JVM参数指定是否开启逃逸分析,-XX:+DoEscapeAnalysis:表示开启逃逸分析-XX:-DoEscapeAnalysis:表示关闭逃逸分析已经开启逃逸分析从jdk1.7开始默认,如果需要禁用,需要指定-XX:-DoEscapeAnalysissynchronizationomission动态编译同步块时,JIT编译器可以通过逃逸分析判断同步块使用的锁对象是否可以只能被一个线程访问,并没有被释放给其他线程。如果同步块使用的锁对象通过这个分析确定只能被一个线程访问,那么JIT编译器在编译同步块的时候就会取消这部分代码的同步。这种取消同步的过程称为同步消除,也称为锁消除。比如下面的代码:publicvoidf(){Objecthollis=newObject();synchronized(hollis){System.out.println(hollis);}}代码锁定了对象hollis,但是hollis对象的生命周期只有在f()方法中,不会被其他线程访问,所以会在JIT编译阶段进行优化。优化为:publicvoidf(){Objecthollis=newObject();System.out.println(hollis);}因此,在使用synchronized时,如果JIT经过逃逸分析后发现没有线程安全问题,就会做锁淘汰。ScalarReplacementScalar(标量)是指一个数据不能被分解成更小的数据。Java中的原始数据类型是标量。相比之下,那些可以分解的数据称为聚合,Java中的对象是聚合,因为它们可以分解为其他聚合和标量。在JIT阶段,如果经过逃逸分析,发现某个对象不会被外界访问,那么经过JIT优化后,会将这个对象拆解成包含在它里面的几个成员变量来代替它。此过程称为标量替换。publicstaticvoidmain(String[]args){alloc();}privatestaticvoidalloc(){Pointpoint=newPoint(1,2);System.out.println("point.x="+point.x+";point.y="+point.y);}classPoint{privateintx;privateinty;}在上面的代码中,点对象没有逃脱alloc方法,点对象可以拆解成标量。然后,JIT不会直接创建Point对象,而是直接使用两个标量intx,inty来替换Point对象。上面的代码,经过标量替换后,会变成:privatestaticvoidalloc(){intx=1;inty=2;System.out.println("point.x="+x+";point.y="+y");}可以看到,对Point的聚合量进行逃逸分析后,发现并没有逃逸,所以换成了两个聚合量。那么标量替换有什么好处呢?也就是说,它可以大大减少堆内存的使用。因为一旦不需要创建对象,那么就不需要分配堆内存。标量替换为栈上分配提供了良好的基础。栈分配在Java虚拟机中,对象是在Java堆中分配内存的,这是常识。但是有一种特殊情况,就是如果经过逃逸分析,发现某个对象没有逃逸方法,那么可能会优化分配到栈上。这消除了在堆和垃圾收集上分配内存的需要。关于在栈上分配的详细介绍,可以参考对象不一定在堆上分配内存。还是有必要简单说一下,其实在现有的虚拟机中,并没有真正实现栈上的分配。并非所有的内存都分配在堆上在我们的例子中,对象并没有分配在堆上,但它实际上是通过标量替换实现的。逃逸分析还不成熟。逃逸分析的论文发表于1999年,但直到JDK1.6才实现,而且这项技术到现在也不是很成熟。其根本原因在于,不能保证逃逸分析的性能消耗会比他的消耗高。虽然经过逃逸分析,标量替换,栈分配,锁消除都可以完成。但是逃逸分析本身也需要一系列复杂的分析,这其实是一个比较耗时的过程。一个极端的例子是,经过逃逸分析,发现没有不逃逸的对象。那么逃逸分析的过程就白费了。虽然这项技术还不是很成熟,但它也是即时编译器优化技术的一个非常重要的手段。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文
