JAVA VM(HotSpot)

VM 运行模式

JVM有两种运行模式Server与Client

  1. Client VM:为在客户端环境中减少启动时间而优化;比较适合桌面程序,它会做一些例如像快速初始化,懒加载这一类的事件来适应桌面程序的特点(C1轻量级编译器)
  2. Server VM:为在服务器环境中最大化程序执行速度而设计; 适合做服务器程序,一些针对服务器特点的事情,比如预加载,尤其在一些并发的处理上,是会做更多的优化(C2编译器)

运行模式查看

  1. java -version
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
  1. JAVA_HOME \ jre1.8.0_141 \ lib \ amd64 \ jvm.cfg
-server KNOWN
-client IGNORE

运行模式切换

首先确认JDK支持哪一种或两种模式,查看JAVA_HOME \ jre1.8.0_141 \ bin目录下是否存在client或server目录,分别对应着各自的JVM,64位虚拟机只支持server模式,没有client目录

程序执行过程

在这里插入图片描述

编译

检查是否有语法错误,如果没有就将其翻译成JVM可识别的字节码文件(即.class文件),把源代码(高级语言)转换成(翻译)低级语言(机器语言)的程序

  1. 前端编译器:主要是词法分析、语法分析、语义分析,然后生成一个中间表达形式(IR:Intermediate Representation)(即.class文件),例如:Sun的javac、Eclipse的JDT的增量式编译器

  2. 后端编译器:主要是把中间表达形式IR(即.class文件)进行优化,最终生成目标机器码

后端编译器在JVM中有两种:JIT(Just In Time Compiler)及时、AOT(Ahead Of Time Compiler) 提前

在这里插入图片描述

运行

JVM分配内存,首先会逐条读取IR的指令来执行,这个过程就是解释执行的过程。当某一方法调用次数达到即时编译定义的阈值时,就会触发即时编译,这时即时编译器会将IR进行优化,并生成这个方法的机器码,后续再调用这个方法时,就会直接调用机器码执行,这个就是编译执行的过程

在这里插入图片描述

JIT

JIT编译器_百度百科

JIT编译器,英文写作Just-In-Time Compiler,中文意思是即时编译器。

JIT是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,而动态解释执行的则是一句一句边运行边翻译。

工作原理

在这里插入图片描述

HotSpot VM JIT

  • HotSpot虚拟机中内置了两个JIT编译器:Client Complier(C1编译速度)和Server Complier(C2编译质量),分别用在客户端和服务端,Java10以后新增了一个编译器Graal

  • C1编译器对应参数 -client,对于执行时间较短,对启动性能有要求的程序,可以选C1

  • C2编译器对应参数 -server,对峰值性能有要求的程序,可以选C25

  • 分层编译(Tiered Compiation)工作模式出现之前,HotSpot VM 默认采用解释器与其中一个编译器直接配合的方式工作

  • 分层编译,分层编译时C1和C2有可能同时工作,使用-XX:+TieredCompilation参数开启,它综合了C1的启动性能和C2的峰值性能优势。

  • JDK8默认开启了分层编译

  • 编译级别有:
    0:解释执行
    1:简单C1编译代码
    2:受限的C1编译代码
    3:完全C1编译代码
    4:C2编译代码

运行模式

  1. 混合模式(mixed),默认方式,解释器 + 其中一个JIT编译器
java -version

java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
  1. 解释模式
java -Xint -version

java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, interpreted mode)
  1. 编译模式

优先采用编译,无法编译时也会解释执行(在最新的HotSpot中此参数被取消)

java -Xcomp -version

java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, compiled mode)
  1. 测试
public class JITDemo {
    private static Random random = new Random();
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int count = 0;
        int i = 0;
        while (i++ < 99999999){
            count += plus();
        }
        System.out.println("time cost : " + (System.currentTimeMillis() - start));
    }
    private static int plus() {
        return random.nextInt(10);
    }
}

解释执行模式结果:

-Xint -XX:+PrintCompilation
time cost : 44619

编译执行模式结果:

-Xcomp -XX:+PrintCompilation
1080    1     n 0       sun.misc.Unsafe::objectFieldOffset (native)   
   1081    2     n 0       java.security.AccessController::doPrivileged (native)   (static)
   1087    3   !b  3       java.lang.invoke.MethodHandle::<clinit> (45 bytes)
   1088    4   !b  4       java.lang.invoke.MethodHandle::<clinit> (45 bytes)
   1088    5    b  3       java.lang.ClassLoader$NativeLibrary::getFromClass (13 bytes)
   1089    3   !   3       java.lang.invoke.MethodHandle::<clinit> (45 bytes)   made not entrant
   1089    6    b  4       java.lang.ClassLoader$NativeLibrary::getFromClass (13 bytes)

混合模式结果:

-XX:+PrintCompilation
   1038  110       3       java.util.Hashtable$Entry::<init> (26 bytes)
   1038  113       3       java.lang.StringBuffer::<init> (6 bytes)
   1039  114  s    3       java.lang.StringBuffer::toString (36 bytes)
   1419   62 %     4       com.xxx.config.JITDemo2::main @ -2 (58 bytes)   made not entrant
   1421  115 %     3       com.xxx.config.JITDemo2::main @ 9 (58 bytes)
   1423  116 %     4       com.xxx.config.JITDemo2::main @ 9 (58 bytes)
   1426  115 %     3       com.xxx.config.JITDemo2::main @ -2 (58 bytes)   made not entrant
   2372  116 %     4       com.xxx.config.JITDemo2::main @ -2 (58 bytes)   made not entrant
time cost : 1678

编译结果参数

第1列:为JVM启动后到该方法被编译相隔的时间,单位为毫秒
第2列:编译ID,用来跟踪一个方法的编译、优化、深度优化
第3列:

b    Blocking compiler (always set for client)
*    Generating a native wrapper
%    On stack replacement (where the compiled code is running)
!    Method has exception handlers
s    Method declared as synchronized
n    Method declared as native
made non entrant    compilation was wrong/incomplete, no future callers will use this version
made zombie         code is not in use and ready for GC

第4列 com.xxx.config.JITDemo2::main:被编译的方法
第5列 (67 bytes):方法的字节大小

何时触发即时编译(混合模式)

热点代码判定方式

  • 基于采样的热点探测
    采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  • 基于计数器的热点探测 (HotSpot虚拟机中使用)
    采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。

基于计数器的热点探测方法

  1. 方法调用计数器
    方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。
  2. 回边计数器
    用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”。

JIT编译。触发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。

在这里插入图片描述

测试

  • JVM在调用一个方法时,会在计数器上+1,如果方法里面有循环体,每次循环,计数器也会+1

  • 在不启用分层编译时,当某一方法的计数器达到由参数-XX:CompileThreshold指定的阈值时(C1为1500,C2为10000),就会触发即时编译

  1. 根据方法调用触发(不涉及循环)
package com.xxx.config;

import java.util.Random;
// 参数:-XX:+PrintCompilation -XX:-TieredCompilation(关闭分层编译)
public class JITDemo2 {

    private static Random random = new Random();

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int count = 0;
        int i = 0;
        while (i++ < 15000){
            System.out.println(i);
            count += plus();
        }
        System.out.println("time cost : " + (System.currentTimeMillis() - start));
    }

    // 调用时,编译器计数器+1
    private static int plus() {
        return random.nextInt(10);
    }
}

执行结果如下

14562
14563
   2020   78 %           com.xxx.config.JITDemo2::main @ 9 (67 bytes)
14564
...
15000
time cost : 518

由于解释执行时的计数工作并没有严格与编译器同步,所以并不会是严格的10000,其实只要调用次数足够大,就可以视为热点代码,没必要做到严格同步。

  1. 根据循环回边
package com.xxx.config;

import java.util.Random;
// 参数:-XX:+PrintCompilation -XX:-TieredCompilation(关闭分层编译)
public class JITDemo2 {

    private static Random random = new Random();

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        plus();
        System.out.println("time cost : " + (System.currentTimeMillis() - start));
    }

    // 调用时,编译器计数器+1
    private static int plus() {
        int count = 0;
        // 每次循环,编译器计数器+1
        for (int i = 0; i < 15000; i++) {
            System.out.println(i);
            count += random.nextInt(10);
        }
        return random.nextInt(10);
    }
}

执行结果如下

14562
   1655   81 %           com.xxx.config.JITDemo2::plus @ 4 (44 bytes)
14563
...
14999
time cost : 941
  1. 根据方法调用和循环回边

每次方法调用中有10次循环,所以每次方法调用计数器应该+11,所以应该会在差不多大于10000/11=909次调用时触发即时编译

package com.xxx.config;

import java.util.Random;

// 参数:-XX:+PrintCompilation -XX:-TieredCompilation(关闭分层编译)
public class JITDemo2 {
    private static Random random = new Random();

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int count = 0;
        int i = 0;
        while (i++ < 15000) {
            System.out.println(i);
            count += plus();
        }
        System.out.println("time cost : " + (System.currentTimeMillis() - start));
    }

    // 调用时,编译器计数器+1
    private static int plus() {
        int count = 0;
        // 每次循环,编译器计数器+1
        for (int i = 0; i < 10; i++) {
            count += random.nextInt(10);
        }
        return random.nextInt(10);
    }
}

执行结果如下

918   1020   17 
            com.xxx.config.JITDemo2::plus (36 bytes)
919
920
921
   1020 922  18             java.util.Random::nextInt
 (74 bytes)
923
...
9999
   5315   80   !         java.io.PrintStream::println (24 bytes)
   5315   81             java.io.PrintStream::print (9 bytes)
...
15000
time cost : 738

CodeCache

CodeCache是热点代码的暂存区,经过即时编译器编译的代码会放在这里,它存在于堆外内存。

  1. JDK8中server模式默认采用分层编译方式,如果需要关闭分层编译,需要加上启动参数
-XX:-TieredCompilation
  1. 参数设置
初始内存大小,默认2496K
-XX:InitialCodeCacheSize

预留内存大小,默认48M
-XX:ReservedCodeCacheSize
  1. 打印出所有参数的默认值
-XX:+PrintFlagsFinal
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值