JVM基础
java程序---> java字节码 ---java虚拟机解释成--->机器码
机器码是CPU直接读取运行的指令。
字节码是一种中间状态,需要直译后才能成为机器码.
JVM运行模式:
- Client模式启动速度较快,Server模式启动较慢;
- 但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。
- 因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式
- 启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。
执行引擎: 包括 解释器、JIT编译器、垃圾回收器
JIT编译器:属于动态编译方式。 没有JIT之前,每一行Java代码需要去解释然后执行,如果代码执行次数很多(方法被频繁调用n次, client模式下n为1500, server模式下n为10000),这段代码就是热点代码,JIT会把方法编译成机器码。(JIT编译是以整个方法为单位进行编译)。
用计数器来记录调用次数:
- 方法调用计数器: 在方法对象中取存储调用次数
- 回边计数器: 每一次循环回去,都会记录一次次数
JIT优化:
公共子表达式消除
int d = (o*q) * 27 + p + (p + q *o) ---> int d = E * 27 +p + (p + E) --> int d = E * 28 + p * 2
方法内联
将方法调用直接使用方法体重的代码进行替换,这就是方法内联,减少了方法调用过程中压栈与入栈的开销。
private int addNum(int a1, int a2, int a3,int a4) {
return sum(a1,a2) + sum(a3,a4);
}
private int sum(int a, int b){
return a+ b;
}
//方法内联后变为
private int addNum(int a1,int a2, int a3, int a4) {
return a1 + a2 + a3 + a4;
}
逃逸分析
定义: 当一个对象在方法中被定义后,它可能被外部方法所引用,成为方法逃逸。
public class EscapeAnalysis {
//全局变量
public static Object object;
public void globalVariableEscape(){//全局变量赋值逃逸
object = new Object();
}
public Object methodEscape(){ //方法返回值逃逸
return new Object();
}
public void instancePassEscape(){ //实例引用发生逃逸
this.speak(this);
}
public void speak(EscapeAnalysis escapeAnalysis){
System.out.println("Escape Hello");
}
}
再比如:
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1); sb.append(s2);
return sb;
}
//上述方法会产生方法逃逸,如果不想产生逃逸,将 return sb; 改为 return sb.toString();同时将返回值改为String;
注:
从jdk1.7开始已经默认开启逃逸分析; 可以通过JVM参数制定是否开启逃逸分析
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析
对象的栈上分配内存
一般情况下对象的内存分配是在堆上的。但是随着JIT编译优化的发展,对象内存分配也不一定在堆上(可能一部分在堆上,一部分在栈上)。
public class EscapeAnalysisTest {
public static void main(String[] args) {
long a1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
alloc();
}
// 查看执行时间
long a2 = System.currentTimeMillis();
System.out.println("cost " + (a2 - a1) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(100000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
}
static class User { }
}
由于上述代码User对象不会逃逸到alloc外部,经过JIT逃逸分析后,就可以对其内存分配进行优化。
a. 指定JVM参数: 关闭逃逸分析
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
~ jps
2809 StackAllocTest
2810 Jps
~ jmap -histo 2809
num #instances #bytes class name ----------------------------------------------
1: 524 87282184 [I
2: 1000000 16000000 StackAllocTest$User
3: 6806 2093136 [B
4: 8006 1320872 [C
5: 4188 100512 java.lang.String
6: 581 66304 java.lang.Class
看到关闭逃逸分析后,在堆上分配了$User对象100万个。
b. 开启逃逸分析:
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
会看到堆上面创建的User对象会很少,并且耗时也快很多。
标量替换
//有一个类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 class TestLockEliminate {
public static String getString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
public static void main(String[] args) {
long tsStart = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
getString("TestLockEliminate ", "Suffix");
}
System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
}
}
上述getString方法中的sb,不可能逃逸出方法,但是StringBuffer的append方法是有synchronized修饰的。
运行时候: (-XX:-EliminateLocks 锁消除必须运行在 -server 模式下)
-XX:+DoEscapeAnalysis -XX:+EliminateLocks //开启锁消除
-XX:+DoEscapeAnalysis -XX:-EliminateLocks //关闭锁消除
会发现,开启锁消除,执行时间会少。