JVM(一):初识JVM

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
    当然了,同步锁消除默认就是开启的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值