备战之JVM01-初识JVM

初识JVM

一、JVM的运行模式
Server模式与Client模式

两种运行模式的区别:

1、Client模式的启动速度比较快,Server模式启动比较慢。

2、进入运行稳定期之后,Server模式的运行速度要比Client模式运行速度快。

3、Server模式采用的重量级的虚拟机,对程序做了很多的优化。而Client模式采用的是轻量级的虚拟机。

二、JVM架构

在这里插入图片描述
备注:图片源于网络

三、JVM程序的执行流程

在这里插入图片描述
备注:图片源于网络

四、编译器和解释器协调工作流程

在这里插入图片描述
备注:图片源于网络

1、动态编译

JIT编译器属于动态编译 ,也就是在运行时编译,与之相对的就是静态编译,也叫事先编译。

2、什么是热点代码

(1)被多次调用的方法

(2)被多次执行的循环体

这两种情况,编译器都是以整个方法作为编译对象。因为编译发生在方法执行过程中,因此形象的被称为栈上替换(On Stack Replacement),即方法栈帧还在栈上,方法就被替换了。

3、热点代码检测方式

(1)基于采样的热点检测

周期性的检查栈顶,如果某个方法经常出现在栈顶,则被认为是热点代码。简单高效,但是易受线程阻塞影响。

(2)基于计数器的热点检测(Hot Spot采用的方式)

方法调用计数器:统计方法被调用的次数,Server模式下是10000次,可以通过虚拟机参数

-XX:CompileThreshold设置。
在这里插入图片描述

回边计数器:统计循环体执行的次数,在字节码中遇到向后跳转的指令,称之为回边。

五、JIT的使用
1、为什么使用解释器和编译器共存的架构
解释器和编译器的特点
  • 当程序迅速启动和执行的时候,解释器可以先发挥作用,省去编译的时间,立即执行。程序稳定运行后,编译器发挥作用,越来越多的代码变编译成了本地代码,提高执行效率。
  • 在程序的运行环境中,可以使用解释器节省内存,用编译器提高效率。
编译的时间开销
  • 解释器执行,抽象的看:输入的代码 ->[解释器 解释执行]->执行结果
  • 编译器执行,抽象的看:输入的代码->[编译器 编译]->编译后的代码->[执行]->执行结果

说JIT编译比解释快,是指执行编译后的代码要比解释执行快。并不是编译这个动作比解释快。所以对“只执行一次”的代码而言,解释执行要比JIT编译要快。

怎么算只执行一次呢?粗略的说,下面两个条件同时满足就是严格执行一次

  • 只被调用一次,例如类的构造
  • 没有循环

只有对执行频繁的代码,JIT编译才会有收益。

编译的空间开销

编译后的代码相比字节码代码,膨胀比达到10X是很正常的。所以只有对执行频繁的代码,才值得编译。

2、为什么实现两个不同的即时编译器

HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编

译器,分别用在客户端和服务端。

**目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。**程序使用哪个

编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运

行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

用Client Complier获取更高的编译速度,用Server Complier 来获取更好的编译质量。

3、如何编译为本地代码

Server Compiler和Client Compiler两个编译器的编译过程是不一样的。

对Client Compiler来说,它是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。

而Server Compiler则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。

六、JIT优化
1、公共子表达式消除

如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式

如下代码:

int d = (c*b)*12+a+(a+b*c); 

可以简化为:

int d = E*12+a+(a+E)

这里的E就是公共子表达式。

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; 

} 

可以简化为:

private int add4(int x1, int x2, int x3, int x4) { 

	return x1 + x2 + x3 + x4; 

} 
3、逃逸分析

通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用。

逃逸分析包括:

  • ​ 全局变量赋值逃逸

  • ​ 方法返回值逃逸

  • ​ 实例引用发生逃逸

  • ​ 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量

    代码如下:

    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; 
    
    } 
    

    StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer 有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

    上述代码如果想要StringBuffer sb不逃出方法,可以这样写:

    public static String createStringBuffer(String s1, String s2) { 
        StringBuffer sb = new StringBuffer(); 
        sb.append(s1); 
        sb.append(s2); 
        return sb.toString(); 
    }
    

    不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。

    使用逃逸分析,编译器可以对代码做如下优化:

    一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

    二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对

    象可能是栈分配的候选,而不是堆分配。

    三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象

    的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    在Java代码运行时,通过JVM参数可指定是否开启逃逸分析:

    -XX:+DoEscapeAnalysis : 表示开启逃逸分析

    -XX:-DoEscapeAnalysis : 表示关闭逃逸分析

4、对象栈上分配(基于逃逸分析)

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 {
    } 
}

我们在alloc方法中定义了User对象,但是并没有在方法外部引用他。也就是说,这个对象并不会逃逸到alloc外部。经过JIT的逃逸分析之后,就可以对其内存分配进行优化。

我们指定以下JVM参数并运行:

-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails 
-XX:+HeapDumpOnOutOfMemoryError

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用jmap命令,来查看下当前堆内存中有多少个User对象:

~ 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

从上面的jmap执行结果中我们可以看到,堆中共创建了100万个 StackAllocTest$User 实例。

在关闭逃避分析的情况下(-XX:-DoEscapeAnalysis),虽然在alloc方法中创建的User对象并没有逃逸到方法外部,但是还是被分配在堆内存中。也就说,如果没有JIT编译器优化,没有逃逸分析技术,正常情况下就应该是这样的。即所有对象都分配到堆内存中。

我们指定以下JVM参数并运行:

-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails 
-XX:+HeapDumpOnOutOfMemoryError

接下来,我们开启逃逸分析,再来执行下以上代码。

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用 jmap 命令,来查看下当前堆内存中有多少个User对象:

在这里插入图片描述

从以上打印结果中可以发现,开启了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆内存中只有8万多个 StackAllocTest$User 对象。也就是说在经过JIT优化之后,堆内存中分配的对象数量,从100万降到了8万。

除了以上通过jmap验证对象个数的方法以外,还可以尝试将堆内存调小,然后执行以上代码,根据GC的次数来分析,也能发现,开启了逃逸分析之后,在运行期间,GC次数会明显减少。正是因为很多堆上分配被优化成了栈上分配,所以GC次数有了明显的减少。

所以,如果以后再有人问你:是不是所有的对象和数组都会在堆内存分配空间?

那么你可以告诉他:不一定,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。就像我们前面看到的一样,在开启逃逸分析之后,也并不是所有User对象都没有在堆上分配。

5、标量替换(基于逃逸分析)

标量:无法再被分解成更小数据的数据。

在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; 
}
//这就是标量替换
6、同步锁消除(基于逃逸分析)

同样基于逃逸分析,当加锁的变量不会发生逃逸,是线程私有的完全没有必要加锁。 在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()方法中的StringBuffer数以函数内部的局部变量,作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作,代码如下:

@Override 
public synchronized StringBuffer append(String str) { 
    toStringCache = null; 
    super.append(str);
    return this; 
}

逃逸分析和锁消除分别可以使用参数 -XX:+DoEscapeAnalysis 和 -XX:+EliminateLocks (锁消除必须在-server模式下)开启。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值