Jit编译:just in time 编译. Java代码只有在执行一段时间以后才会进行jit编译。
Hotspot会编译优化那些热点代码,以求最大的性能收益。
Jit编译的好处:
1. 执行一段时间后,可以统计出哪些代码的调用频次高。
2. 执行一段时间后,编译器可以获得代码的一些性能信息,来加大编译优化的力度。所以,现在的jit编译优化甚至可能比C语言的编译优化做的还要效果好。
jit编译器有俩种:C1(client),和C2(Server) 俩种编译器。
C1:编译的时机要比C2快,编译的代码要比C2多。好处了可以在程序启动刚开始就能获得比较好的性能。应用启动时间快。
C2:编译的时机要比C1晚,因为它希望可以获得更多的统计信息,进行优化程度更高的优化处理。应用启动时间慢。
分层编译:程序在刚启动的时候进行C1编译,随着代码执行的时间增加,再慢慢进行C2编译。
应用运行的时间短就采用C1,否则使用C2。一般来说,用分层编译总是没错的。
Jit编译后的字节码会保存在code-cache中,而code-cache的大小是有限的。使用C1或者分层编译所编译的代码较多,比较容易填满codecache,而C2编译的代码量不会那么多。可以通过-XX:ReservedCodeCacheSize=N 来设置code-cache的最大值。需要注意的是,任何内存区都可能进行内存保留,所以把最大值设大,可能会导致占用过多的内存。也就是因为内存的消耗,所以,我们需要考虑机器资源来权衡jit编译的程度,来最大化应用的性能。
JVM有俩个参数:方法调用计数器和循环回边计数器。
标准编译:当方法调用计数器和循环回边计数器记录的总次数超过一定的阈值,就对一个方法进行jit编译。
栈上替换:如果方法计数器的值没有达到阈值,但是循环回边计数器达到的一定的值,会对这个循环进行编译,而不会对整个方法进行编译,并在方法栈上进行替换。
C1和C2在编译时机上的不同主要是由于这俩个计数器的阈值是不同的。另外,计数器的值还会周期性的减少,所以它表示的是最新的调用热度。jit编译的阈值是可以调节的,但是要考虑到调整完后对code-cache带来的消耗。
对jit编译情况进行统计的方法有:
1. 开启 -XX:+PrintCompilation
2. Jstat -compiler pid
jstat -printcompilation
编译线程:编译的操作是异步的,会有编译线程在后台对达到要求的代码进行编译。线程分为client线程和server线程,分别用于C1,C2编译器。分层编译俩种线程都有。编译线程的量是可调节的,但是这通常影响的是应用在热身期的性能。如果过了热身期,这些编译线程就不会在占用cpu了。
内联:内联带来的性能提升是巨大的,一方面是内联本身带来的方法调用的减少。另一个是否重要的方面是,内联后的代码,又可以促进很多其他优化。内联的关闭-XX:-Inline (默认是开的)
常规的内联:当方法很小时会进行内联。小于35个字节或-XX:MaxInlineSize=N 所设的值。
频繁调用带来的内联:当调用很频繁时,如果小于325个字节或者-XX:MaxFreqInlineSize=N 所设定的值。
逃逸分析(-XX:+DoEscapeAnalysis,默认为 true) ,编译器会做一些非常复杂和激进的优化。比如把一些没有用到的变量的计算省略掉。
逆优化:指编译器对一些编译进行撤回。有两种逆优化的情形:代码状态分别为“made not entrant”(代码被丢弃)和“made zombie”(产生僵尸代码)
“made not entrant”(代码被丢弃)的发生有俩种情况:
1. 如果一个方法内部的一个逻辑分支一直被调用,然后进行了jit编译,如果这时出现了另一个逻辑分支的调用,就会导致原来的编译代码失效,然后被丢弃。在编译的详情日志里也会出现made not entrant,然后会出现made zombie
2. 在分层编译中,C1编译的代码,接着被C2编译后,之前的jit编译的代码就会被丢弃。
“made zombie”(产生僵尸代码) :指上面被丢弃的代码被GC回收了。
分层编译级别:
- 0:解释代码
- 1:简单 C1 编译代码
- 2:受限的 C1 编译代码
- 3:完全 C1 编译代码
- 4:C2 编译代码
C1和C2都有自己的编译队列,存储达到阈值的需要编译的代码。如果C2的队列满了,则这段代码会进行2,然后进行C1编译。等C2队列空闲后再进行C2编译。同样,如果C1编译队列满了,也会进行类似的操作。