JVM架构理解
一张图,讲的很清晰,jvm主要包括了类加载器、运行时数据区、执行引擎
程序的执行过程
首先得知道我们的java程序写好之后是如何执行的?
java代码先被编译成字节码文件,也就是.class文件,我们的JVM只认识字节码文件,它会将我们的字节码文件编译成机器码文件,而这个机器码文件才是电脑CPU能直接读取运行的指令。
在这过程中,JVM帮我们屏蔽了不同操作系统间的差别,所以我们只要一次编译,只要有.class文件,就能实现在不同平台上的运行。
具体的过程如下:
.class文件被类加载器加载进JVM,通过执行引擎去执行,不同的数据被分配在运行时数据区的不同区域。
执行引擎
图中可以看到执行引擎包含了解释器、JIT编译器、垃圾回收。
那么什么是解释器,什么又是JIT编译器呢?为什么解释器和JIT要并存呢?什么时候用解释器,又什么时候用JIT编译器?JIT编译器有两种,为什么设计两种呢?
解释器和JIT编译器
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文统称JIT编译器。
如何判断是热点代码呢?被多次调用的方法或者被多次执行的循环体
有两种判定方法:方法调用计数器和回边计数器
-
方法调用计数器
在JVM client模式下的阀值是1500次,Server是10000次。可以通过虚拟机参数: -XX:
CompileThreshold设置。但是JVM还存在热度衰减,时间段内调用方法的次数较少,计数器就减小。
-
回边计数器
它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。
简单点讲:执行次数少的代码用解释器,减少空间开销;执行次数多的代码用JIT编译器,减少时间开销。所以JVM选择的是解释器+JIT编译器的混合执行引擎。
JIT编译器
HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编
译器,分别用在客户端和服务端。默认是采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式。(JVM运行模式有Server模式与Client模式)
JIT优化
JIT编译器在编译的时候会对我们的代码做一个优化,简单介绍几种优化方案,具体的可以见官网。
-
公共子表达式的消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对他进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。
比如int d = (c*b)*12+a+(a+b*c);
这段代码,b*c可以做优化成int d = E*12+a+(a+E);
,然后可以进一步优化合并成int d = E*13+a*2;
经过这样的一些优化后,计算起来就能节省时间了。 -
方法内联
将方法调用直接使用方法体中的代码进行替换,这就是方法内联,减少了方法调用过程中压栈与入栈的开销。
比如下面这个代码:private int add4(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4); } private int add2(int x1, int x2) { return x1 + x2; }
可以肯定的是运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:
private int add4(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }
-
逃逸分析
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。简单点讲就是,一个 变量的只可能在自己方法内部,不会暴露给方法外其他地方使用。
逃逸分析包括:
1.全局变量赋值逃逸
2.方法返回值逃逸
3.实例引用发生逃逸
4.线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,从jdk 1.7开始已经默认开始逃逸分析。-XX:+DoEscapeAnalysis : 表示开启逃逸分析 -XX:-DoEscapeAnalysis : 表示关闭逃逸分析
-
对象的栈上内存分配
如果一个对象没有发生方法逃逸,经过JIT的逃逸分析之后,就可以对其内存分配进行优化。这样的话很多堆上分配被优化成了栈上分配,能减少GC次数。
所以,是不是所有的对象和数组都会在堆内存分配空间?
以后我的回答就是不一定,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。就像我们前面看到的一样,在开启逃逸分析之后,也并不是所有对象都没有在堆上分配。 -
标量替换
标量(Scalar)是指一个无法再分解成更小的数据的数据 。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。//有一个类A public class A{ public int a=1; public int b=2 } //方法getAB使用类A里面的a,b private void getAB(){ A x = new A(); x.a; x.b; } //JVM在编译的时候会直接编译成 private void getAB(){ a = 1; b = 2; } //这就是标量替换
-
同步锁消除
同样基于逃逸分析,当加锁的变量不会发生逃逸,是线程私有的完全没有必要加锁。 在JIT编译时期就可以将同步锁去掉,以减少加锁与解锁造成的资源开销。public static String getString(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
上述方法中的sb对象没有发生方法逃逸,而StringBuffer的append方法是加锁的,所以会把同步锁给消除。使用参数
-XX:+DoEscapeAnalysis -XX:-EliminateLocks
。
当然了,同步锁消除默认就是开启的。