在把class文件加载到JVM中且产生了Class对象后,就可以执行Class对象的静态方法和实例化调用了,源码编译阶段源码被编译成JVM字节码,JVM字节码是作为一种中间码的方式,在运行期需要对它进行解释并执行,这种方式称作字节码解释执行方式。
字节码的解释执行
JVM自己有一套对JVM字节码中的方法的执行指令
Invokestatic:这个指令用于执行类的静态方法
Invokevirtual:这个指令用于执行调用类的实例方法
Invokeinterface:这个指令用于执行调用类中继承自接口的方法
Invokespecial:这个指令用于执行类的private修饰的方法,以及编译源码后生成的<init>方法,此方法为对象实例化时的初始化方法
SUN JDK通过采用栈的体系执行字节码,线程在创建后都会产生程序计数器(PC)和栈,PC存放了下一条要执行的指令在方法内的偏移量。栈中存放了栈帧,每个方法的调用都会产生栈帧,栈帧主要包括两部分,分别是局部变量区和操作数栈,局部变量区包含着方法的参数和局部变量,操作数栈主要存放方法执行过程中产生的中间数,有时栈帧中还会存在一些其他空间,例如指向方法已解析的常量池的引用。
指令解释执行
对于方法的指令的解释执行,遵照冯诺依曼的FDX循环方式,即获取下一条指令,解释并分派,然后执行,后面也做了大量的改进
栈顶缓存
在方法的执行过程中,有很多值都需要放到操作数栈中,这就导致了内存和寄存器(寄存器在CPU内部)要不断的交换数据,Sun JDK采用了栈顶缓存的方式,即将本来位于操作数栈顶的值直接缓存在寄存器中,这样就可以直接在寄存器中计算,然后放回操作数栈
部分栈帧共享
当一个方法调用另一个方法时,通常传入另一方法的参数为操作数栈的数据,就会产生大量的copy操作,Sun JDK做了一个优化,当调用方法时,后一个方法可将前一个方法的操作数栈作为当前方法的局部变量,从而节省copy的消耗
编译执行
解释执行的效率较低,为了提升代码的执行性能,Sun JDK提供将字节码编译为机器码的支持,这个编译在运行时进行,通常称为JIT编译器。Sun JDK对执行效率高的代码进行编译,对执行不频繁的代码进行解释执行的方式。
关于编译Sun JDK提供了两种模式:client compiler 和 server compiler
client compiler (C1)
client compiler又称为C1,较为轻量级,只做少量性能开销比较高的优化,适合于桌面化应用。C1的主要优化有:方法内联、去虚拟化、冗余削除。
方法内联
对于面向对象的语言,通常要调用多个方法来完成功能。执行时要经历多次方法的传递以及跳转等,于是C1采用了方法内联的方式,把要调到的方法的指令植入到当前方法中。
举个例子
public void bar(){
bar2();
private void bar2(){
//bar2
}
方法内联后:
public void bar(){
//bar2
}
去虚拟化
去虚拟化是指在装载class文件后,进行类层次的分析,如果发现类或接口的方法只有一个类实现了该方法,那么对于调用此方法的代码,也可以进行方法内联,例如:
public interface IFoo{
public void bar();
}
public class Foo implements IFoo{
public void bar(){
//Foo bar method
}
public class Demo{
public void execute(IFoo foo){
foo.bar();
)
}
//方法内联后:(当JVM中只有Foo实现了IFoo接口后,Demo的execute方法被编译时)
public void execute(){
//Foo bar method
}
冗余削除
冗余削除是指在编译时,根据运行时状况进行代码折叠或削除。
例如一些if(m){....}的代码返回m为false就会把if语句直接折叠或削除,增加运行时速度
server compiler (C2)
server compiler又称为C2,较为重量级,C2采用了大量编译优化技巧进行优化,占用的内存会更多一点,适合于服务器端的应用。和C1不同的主要是分配策略以及优化范围,由于C2会收集程序的运行信息,因此其优化的范围更多在于全局的优化,而不仅仅是一个方法块的优化,收集的信息主要有:分支的跳转/不跳转,某条指令出现过的类型、是否出现过空值、是否出现过异常。
逃逸分析
逃逸分析是C2进行许多操作的基础,逃逸分析是根据运行状况来判断方法中的变量是否会被外部读取。如过不会则认为此变量是逃逸的。基于逃逸分析C2在编译时会做标量替换、栈上分配和同步削除等优化。
标量替换
标量替换的意思简单来说就是用标量替换聚合量
标量:在java中基础类型不可再分属于标量,同样引用也属于标量,而对象本身则是聚合量。
例子:
Point point=new Point(1,2);
System.out.println("point.x="+point.x+";point.y="+point.y);
//当point对象里之后的执行过程(方法什么的)未用到时,经过编译标量替换后后
int x=1;
int y=2;
System.out println("point.x="+x+";point .y="+y);
这样做的好处是,如果创建的对象并未用到全部变量,则可以节省一定的内存。
栈上分配
在上面的例子中,如果point没有逃逸,那么C2会选择在栈上直接创建Point对象实例,而不是在JVM堆上。在栈上分配的好处就是更加快速,另一方面是回收时随着方法结束了,对象也被回收了。
同步削除
同步削除是指如果发现同步的对象未逃逸,那也没有同步的必要了,在C2中会直接去掉同步
例子:
Point point=new Point (1,2);
synchronized(point){
//do something
}
//如果发现point未逃逸
Point point=new Point (1,2);
//do something