JIT(即时编译)
解释型代码:程序可移植,相同的代码在任何有适当解释器的机器上,都能运行,但是速度慢。
编译型代码:速度快,电视不同CPU平台的代码无法兼容。
java则是使用java的编译器先将其编译为class文件,也就是字节码;然后将字节码交由jvm(java虚拟机)解释执行。由于这个编译是在程序执行时进行的,因此被称为“即使编译”。
热点编译
对于程序来说,通常只有一部分代码被经常执行,而应用的性能就取决于这些代码执行得有多快。这些关键代码段被称为应用的热点,代码执行得越多就被认为是越热。
因此JVM执行代码时,并不会立即编译代码:
- 如果代码只执行一次,那编译完全就是浪费精力。对于只执行一次的代码,解释执行Java字节码比先编译然后执行的速度快。
- JVM执行特定方法或者循环的次数越多,它就会越了解这段代码。这使得JVM可以在编译代码时进行大量优化。
分层编译
Client编译器和server编译器主要的区别在于编译代码的时机不同。client编译器开启编译比server编译器要早。这意味着在代码执行的开始阶段,client编译器比server编译器要快,因为它的编译代码相比server编译器而言要多。
分层编译是综合了client和server的优点。在开启分层编译(-XX:+TieredCompilation)后代码先由client编译器编译,随着代码变热,由server编译器重新编译。
调优代码缓存
JVM编译代码时,会在代码缓存中保留编译之后的汇编语言指令集。代码缓存的大小固定,所以一旦填满,JVM就不能编译更多代码了。
也就是说,如果代码缓存过小,那么就会有一些热点代码被编译了,而其他没有,最终导致应用的大部分代码都是解释运行(非常慢)。这个问题在使用client编译器或进行分层编译时很常见。
当代码缓存填满时,JVM通常会发出以下警告:
Java HotSopt(TM) 64-Bit Server VM warning:CodeCache is full.Compiler has bean disabled.
Java HotSopt(TM) 64-Bit Server VM warning:Try increasing the code cache size using -XX:ReservedCodeCacheSize=
各平台代码缓存的默认大小:
jvm | jdk版本 | 大小 |
---|---|---|
32位client | Java8 | 32MB |
32位client | 分层编译,Java8 | 240MB |
64位client | 分层编译,Java8 | 240MB |
32位client | Java7 | 32MB |
32位server | Java7 | 32MB |
64位server | Java7 | 48MB |
64位server | 分层编译,Java7 | 48MB |
如果代码缓存设为1GB,JVM就会保留1GB的本地内存空间。如果是32位JVM,那么进程占用的总内存不能超过4GB(包括Java堆、JVM自身所有代码占用空间、分配给应用的本地内存、代码缓存)。
通过jconsole Memory(内存)面板的Memory Pool Code Cache图表,可以监控代码缓存。
编译阈值
一旦代码执行到一定次数,且达到了编译阈值,编译器就可以获得足够的信息编译代码了。
编译是基于两种JVM计数器的:方法调用计数器和方法中的循环回边计数器。回边实际上可以看作是循环完成执行的次数。
栈上替换:JVM可以在方法循环运行时进行编译,并在循环代码编译结束之后,JVM替换还在栈上的代码,循环的下一次迭代就会执行快的多的代码。
标准编译由-XX:CompileThreshold=N标志触发。使用client编译器时,N的默认值是1500,使用server编译器时为10000。
计数器会随着时间而减少,所以计数器只是方法或循环最新热度的度量。由此带来一个副作用是,执行不太频繁的代码可能永远不会编译。
检测编译过程
-XX:+PrintCompilation
如果开启PrintCompilation,每次编译一个方法(或循环)时,JVM就会打印一行被编译的内容信息。
绝大多数编译日志的行具有以下格式:
timestamp compilation_id attributes (tiered_level) method_name size deopt
timestamp表示编译完成的时间
compilation_id内部的任务ID
attributes是一组5个字符长的串,表示代码编译的状态。如果给定的编译被赋予了特定属性,就会打印下面列表中所显示的字符,否则该属性就打印一个空格。
* % :编译为OSR
* s :方法是同步的
* !:方法有异常处理器
* b :阻塞模式时发生的编译
* n:为封装本地方法所发生的编译
tiered_level 如果程序没有使用分成编译的方式运行则为空,否则为数字,表明所完成编译的级别
method_name格式为:ClassName::method
然后是编译后代码大小(单位是字节)
最后,在某些情况下,编译日志的结尾会有一条信息,表明发生了某种逆优化,通常是“made not entrant”或”made zombie”
135 1 n 0 java.lang.Thread::currentThread (native) (static)
136 2 3 java.util.Arrays::copyOf (19 bytes)
136 7 3 sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
137 8 2 java.lang.String::hashCode (55 bytes)
使用jstat -compiler 进程ID 也可以看有多少方法被编译
使用jstat -printcompilation 5003 1000 表示进程ID为5003的程序每1秒输出一次最近被编译的方法
编译器线程
当方法(或循环)适合编译时,就会进入到编译队列。队列则由一个或多个后台线程处理。编译队列是一种优先队列,即调用计数次数多的方法有更高的优先级。
当开启分层编译时,JVM默认开启多个client和server线程。
cpu数量 | C1的线程数(client) | C2的线程数(server) |
---|---|---|
1 | 1 | 1 |
2 | 1 | 1 |
4 | 1 | 2 |
8 | 1 | 2 |
16 | 2 | 6 |
32 | 3 | 7 |
64 | 4 | 8 |
128 | 4 | 10 |
编译器的线程数可通过-XX:CICompilerCount=N
标志来设置。对于分层编译来说,设置的值中三分之一将用来处理client编译器队列,其余的线程(至少一个)用来处理server编译器队列。
使用分层编译时,线程数很容易超过系统限制,特别是有多个JVM同时运行的时候。在这种情况下,减少线程数有助于提高整体的吞吐量(尽管代价可能是热身期会持续得更长)。
方法内联
public class Point{
private int x,y;
public int getX(){ return x; }
public void setX(int i){ x = i;}
}
如果你写下面的代码
Point p = getPoint();
p.setX(p.getX()*2);
编译后的代码本质上执行的是:
Point p = getPoint();
p.x = p.x *2;
方法是否内联取决于它有多热以及它的大小。
-XX:MaxInlineSize=N
默认是35字节,即只有方法小于35字节时第一次调用方法时就会被内联。
-XX:MaxFreqInlineSize=N
默认是325字节,即只有当一个方法频繁被调用并且小于325字节时会被内联。
逃逸分析
-XX:+DoEscapeAnalysis
默认为true。逃逸分析可以让JVM对一个对象根据代码来进行优化。
- 栈上分配
我们都知道Java中的对象都是在堆上分配的,而垃圾回收机制会回收堆中不再使用的对象,但是筛选可回收对象,回收对象还有整理内存都需要消耗时间。如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。 - 同步消除
如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。 - 标量替换
Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
小结
- 不用担心小方法,特别是getter和setter,因为它们容易内联。
- 需要编译的代码在编译队列中,队列中代码越多,程序打到最佳性能的时间越久。
- 虽然代码缓存的大小可以调整,但它仍然是有限的资源
- 代码越简单,优化越多。