Java性能优化指南系列(三):理解JIT编译器

时间:2016-07-13 16:10:18   收藏:0   阅读:1346

即时编译器概述

编译热点代码

基本调优:Client Server

启动优化

                  技术分享

可以看到,对于中等大小的GUI应用,使用client编译器,启动时间最短,有将近38.5%的提升。对于大型应用,使用各种编译器的启动时间差别不大。

批处理应用优化

                    技术分享

观察上图,可以得出以下结论:

1)当股票数比较少的时候(1~100),client编译器完成的最快。

2)当股票数很多的时候,使用server编译器就变得更快了。

3tiered compilation总是比server编译器要快(和client比,即使在股票数很少的情况下,性能相差也不大),这是因为,tieredcompilation会对一些执行次数较少的代码也进行编译(编译后比解释执行要快)。

长时间运行的应用优化

             技术分享

        观察上图,可以得出以下结论:

        1)由于测试周期是60秒,所以,即使warm-up period0sserver编译器也有足够的时间来发现热点代码并进行编译,所以,server编译器总是比client编译器的吞吐量要高。

        2)同样的tiered compilation总是比server编译器的吞吐量要高(原因见上文,批处理应用优化)。

Java和JIT 编译器版本

答案并不是这样的,而是需要根据情况确定。

使用32bit版本的优点主要有两个:1)占用内存少,因为对应引用都是32位的 2)性能高,因为CPU操作32位的内存引用要比操作64位的内存引用要快。缺点也有两个:1)使用堆内存的大小不能超过4GB(windows为3GB,Linux为3.5GB) 2)程序中,如果使用了大量的longdouble变量,不能充分使用64位寄存器,不过这种情况比较少见。  

一般来说,32bit JVM上的32bit编译器要比同样配置的64bit编译器要快5%~20%

技术分享

上图是对不同平台下,使用不同参数对应的编译器。

技术分享

编译器的中间段优化

Code Cache优化

Java HotSpot(TM) 64-Bit Server VM warning: CodeCacheis full.

Compiler has been disabled.

Java HotSpot(TM) 64-Bit Server VM warning: Tryincreasing the

code cache size using -XX:ReservedCodeCacheSize=

技术分享

从上图可以看到,Java7Code Cache 通常是不够的,一般都需要进行加大。到底增大到多少,这个比较难给出精确值,一般是默认值的2倍或4倍。

编译阈值

探寻编译过程

timestamp compilation_idattributes (tiered_level) method_name size deopt

小技巧:使用jstat来查看编译器的行为

% jstat -compiler 5003

Compiled Failed Invalid   Time   FailedTypeFailedMethod

206           0                   0   1.97                    0

5003JVM的进程ID,通过这个命令可以看到有多少个方法被编译了,编译失败有多少个,最后编译失败的方法名字是什么。

% jstat -printcompilation 50031000

CompiledSize Type Method

20764 1 java/lang/CharacterDataLatin1 toUpperCase

2085 1 java/math/BigDecimal$StringBuilderHelper getCharArray

还可以通过上面的命令来周期性(1000的单位为ms,也就是1秒执行一次)的打印编译信息,这样可以看到最后编译的方法名称。

timestamp compile_id COMPILESKIPPED: reason

发生错误的原因主要有两个:

1Code cache filledCode Cache空间已经满了,需要使用 ReservedCodeCache标志来增加空间

2Concurrentclassloading:在编译的过程中,class发生了修改,JVM将会在后面重新编译它。

28015 850 net.sdo.StockPrice::getClosingPrice (5bytes)

28179 905 s net.sdo.StockPriceHistoryImpl::process(248 bytes)

28226 25 %net.sdo.StockPriceHistoryImpl::<init> @ 48 (156 bytes)

28244 935net.sdo.MockStockPriceEntityManagerFactory$MockStockPriceEntityManager::find(507 bytes)

29929 939net.sdo.StockPriceHistoryImpl::<init> (156 bytes)

106805 1568 ! net.sdo.StockServlet::processRequest(197 bytes)

通过上面的日志,可以得到下面的结论:

1)第一个方法getClosingPrice直到应用启动后的28s才进行了编译,之前已经有849个方法进行了编译。

2)process方法是synchronized

3)内部类的方法也会单独显示,比如:net.sdo.MockStockPriceEntityManagerFactory$MockStockPriceEntityManager::find

4processRequest方法有异常处理

5)从StockPriceHistoryImpl的实现看,其中有一个大的loop,

public StockPriceHistoryImpl(String s, Date startDate, Date endDate) {

EntityManager em = emf.createEntityManager();

Date curDate = new Date(startDate.getTime());

symbol = s;

while (!curDate.after(endDate)) {

StockPrice sp = em.find(StockPrice.class, new StockPricePK(s, curDate));

if (sp != null) {

if (firstDate == null) {

firstDate = (Date) curDate.clone();

}

prices.put((Date)curDate.clone(), sp);

lastDate = (Date) curDate.clone();

}

curDate.setTime(curDate.getTime() + msPerDay);

}

}

这个loop执行的次数比构造函数本身多很多,因此这个loop会被采用OSR进行编译。因为OSR编译比较复杂(要在代码同时执行的时候进行编译,还要进行栈上替换),所以虽然它的编译ID很小(25,表明比较早就启动了编译),但是经过了较长时间才在编译日志中打印出来。

编译器的高级调优

编译线程

技术分享

可以使用 -XX:CICompilerCount=N来调整编译线程的数目;如果使用的是tiered compilation,那么配置的1/3线程用于C1队列,其余的用于C2队列.

内联

我们可以通过增加这个大小,以便更多的方法可以进行内联;不过一般情况下,调优这个参数对于服务端应用的性能影响不大。

逃逸分析

比如:下面是一个阶乘类,负责存放阶乘的值和起始值。

public class Factorial {

private BigIntegerfactorial;

private int n;

 

public Factorial(int n) {

this.n = n;

}

 

public synchronizedBigInteger getFactorial() {

if (factorial == null)

factorial = ...;

return factorial;

}

}

将前100个数的阶乘存入数组:

ArrayList<BigInteger> list = new ArrayList<BigInteger>();

for (int i = 0; i < 100; i++){

Factorial factorial = new Factorial(i);

list.add(factorial.getFactorial());

}

对象factorial只会在for循环内部使用,于是JVM会对这个对象做比较多的优化:

1)不需要对函数getFactorial加锁

2)不需要将字段n保存在内存中,可以存放在寄存器中;同样的,factorial对象也可以保存在寄存器中

3)实际上JVM不会分配任何factorial对象,只需要维护每个对象的字段即可。

逃逸分析默认是打开的,只有在极少数的情况下,逃逸分析会出现问题。

反优化

Not Entrant Code

StockPriceHistory sph;

String log = request.getParameter("log");

 

if (log != null &&log.equals("true")) {

sph = new StockPriceHistoryLogger(...);

}

else {

sph = new StockPriceHistoryImpl(...);

}

 

// Then the JSP makes calls to:

sph.getHighPrice();

sph.getStdDev();

// and so on

如果开始有大量的 http://localhost:8080/StockServlet 调用(没有log参数),对象sph的真正类型就是StockPriceHistoryImpl JVM将会对这个类的构造函数进行内联,并做其它优化。

一段时间之后,有一个调用传入了log参数 http://localhost:8080/StockServlet?log=true 这会导致之前的优化都变得无效(因为实现类发生了变化,变成StockPriceHistoryLogger 。于是JVM会对原来编译的代码进行反优化,即:丢弃原来编译的代码,并对代码重新进行编译,然后替换原来编译的代码。上述过程的编译日志大致如下所示:

841113 25 %net.sdo.StockPriceHistoryImpl::<init> @ -2 (156 bytes) made not entrant

841113 937 snet.sdo.StockPriceHistoryImpl::process (248 bytes)  made not entrant

1322722 25 %net.sdo.StockPriceHistoryImpl::<init> @ -2 (156 bytes)  made zombie

1322722 937 snet.sdo.StockPriceHistoryImpl::process (248 bytes) made zombie

可以看到,OSR编译的构造函数和标准编译的process函数都首先进入made not entrant状态,一段时间后,进入到made zombie状态。

从名字上看,反优化不是一个好的事情,它应该会影响应用的性能。不过,根据实践证明,反优化对性能的影响有限。

40915 84 % 3net.sdo.StockPriceHistoryImpl::<init> @ 48 (156 bytes)

40923 3697 3net.sdo.StockPriceHistoryImpl::<init> (156 bytes)

41418 87 % 4net.sdo.StockPriceHistoryImpl::<init> @ 48 (156 bytes)

41434 84 % 3net.sdo.StockPriceHistoryImpl::<init> @ -2 (156 bytes) made not entrant

41458 3749 4net.sdo.StockPriceHistoryImpl::<init> (156 bytes)

41469 3697 3net.sdo.StockPriceHistoryImpl::<init> (156 bytes) made not entrant

42772 3697 3net.sdo.StockPriceHistoryImpl::<init> (156 bytes) made zombie

42861 84 % 3net.sdo.StockPriceHistoryImpl::<init> @ -2 (156 bytes) made zombie

看到上面有很多not entrantzombie的消息,不要感到惊讶,这些都是正常的,说明JVM编译出了更高效的代码。

反优化Zombie Code

Tiered Complication 级别


评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!