java虚拟机16

深入JVM即时编译器JIT

什么是JIT?

  • just in time compiler,即时编译器

  • Java源码
    字节码Bytecode
    是否是热点代码
    解释执行Interpreter
    编译执行JIT Compiler
    机器码
  • 一般情况是走解释执行,对任何代码启动速度都是一样的,相应的效率就不是很高

  • 但是如果是一个for循环或调用很多的,称为热点代码,就不能再按照解释的形式去执行,而是走JIT

即时编译器

C1
  • 比较简单的即时编译器,关注于局部的优化,适合执行时间比较短,或者对启动速度有要求的程序
  • 也可以称为是client端的编译器
C2
  • 适合执行时间比较长或者对执行峰值有要求的程序
  • servers端的编译器
热点代码怎么判断?
  • 简言之,调用频繁的代码,会把class直接变成机器码,并缓存
    • JVM参数中的CodeCache,-XX:ReservedCodeCacheSize
    • java -XX:+PrintFlagsFinal -version,会把所有虚拟机参数打印出来
    • 默认是200多M
    • 如果空间不够了,JIT就没法编译了,性能会下降一个级别

热点探测

  • J9中jvm是采用采样技术来进行热点探测,但是不精确
  • hotspot是采用计数的方法,为每一个方法建立计数器
方法调用计数器
  • 统计方法被调用的次数,服务端模式下要1万次,
    • -XX:CompileThreshold,默认是10000
回边计数器
  • 统计一个方法中,循环的代码的执行次数

  • 在字节码中,会有循环控制指令,会往前跳,不断的回边

  • 在服务端要触发的话要10700次

    • 回边计数器的阈值=方法调用计数器阈值*OSR比例

    • OSR比例=OnStackReplacePercentage-解释器监控比例(InterpreterProfilePercentage) = 140-33 = 107

  • 建立回边计数器是为了触发OnStackReplace,OnStackReplace是OSR编译,也称为栈上编译,代码中循环次数很多,就会对机器码进行缓存,下次就不会解释了,直接用缓存的机器码替换

分层编译(了解即可)

  • 输入java -version,在最后一行会显示一个mixed mode,这个就代表分层编译
  • 很多情况代码要么是解释执行,要么是JIT,但是jdk8下是混合
  • java -Xint -version,发现就是interpreted mode,即解释模型
  • java -Xcomp -version,发现是compiled mode,即JIT
  • 分层,分成5层
    • 第0层,解释执行,解释执行时要开启性能监控功能
    • 第1层,第0层发现热点代码,用C1编译,可以进行简单、可靠的优化,可以把字节码编译成本地代码
    • 第2层,仍然是C1编译,但是会开启性能监控功能,仅仅包括方法调用次数和回边计数
    • 第3层,还是C1编译,开启所有的性能监控功能
    • 第4层,C2编译,不仅把字节码变成本地代码,还会启动编译耗时比较长的优化,甚至做一些激进的处理

编译优化技术

方法内联

  • 是一种优化方法,是JIT里面做的,把目标代码复制到调用的方法中。比如A方法调用B方法,B方法不执行,直接把B方法放到A方法中
实例
  • package ex16;
    
    /**
     * @author King老师
     * 方法内联
     * -XX:+PrintCompilation   //在控制台打印编译过程信息
     * -XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
     * -XX:+PrintInlining //将内联方法打印出来
     */
    public class CompDemo {
        private int add1(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 add(int x1, int x2, int x3, int x4) {
            return x1 + x2 + x3 + x4;
        }
    
        public static void main(String[] args) {
            CompDemo compDemo = new CompDemo();
            //方法调用计数器的默认阈值10000次,我们循环遍历超过需要阈值
            for (int i = 0; i < 1000000; i++) {
                compDemo.add1(1, 2, 3, 4);
            }
    
        }
    }
    
    
    • add1方法里面调用add2,方法内联会把add1优化成add方法

    • -XX:+PrintCompilation //在控制台打印编译过程信息

    • 要支持打印编译信息,需要开启

      • -XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
    • -XX:+PrintInlining //将内联方法打印出来

  • 在打印的最后,可以看到add1和add2都是hot方法,会触发JIT,并进行方法inline

总结(了解)
  • 方法内联可以提高性能,那怎么提高方法的内联?
    • 1.如果降低方法调用计数器的大小,同时记得调大代码缓存的大小
    • 2.避免在一个方法中写大量的代码,写小方法
    • 3.使用final\static关键字,编译的时候方法会继承,加了这些关键字后,会有额外的类型检查

锁消除

  • 代码中加锁,但是锁会被干掉
实例
  • package ex16;
    
    /**
     * @author King老师
     * 锁消除
     * <p>
     * -XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
     * -XX:-EliminateLocks 关闭锁消除
     */
    public class UnLock {
        public static void main(String[] args) {
            long timeStart1 = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                BufferString("king", "zilu");
            }
            long timeEnd1 = System.currentTimeMillis();
            System.out.println("StringBuffer花费的时间" + (timeEnd1 - timeStart1));
    
            long timeStart2 = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                BuilderString("james", "lison");
            }
            long timeEnd2 = System.currentTimeMillis();
            System.out.println("StringBuilder花费的时间" + (timeEnd2 - timeStart2));
        }
    
        public static String BufferString(String s1, String s2) {
            StringBuffer sb = new StringBuffer();
            sb.append(s1);
            sb.append(s2);
            return sb.toString();
        }
    
        public static String BuilderString(String s1, String s2) {
            StringBuilder sd = new StringBuilder();
            sd.append(s1);
            sd.append(s2);
            return sd.toString();
        }
    }
    
    
  • StringBuffer的append方法,为了确保线程安全加了synchronized,假设是一个单线程,疯狂调用append方法,锁一直在这里,是会有性能损耗的,解释执行会增加一些锁的指令,所以可以把这种锁消除

  • StringBuilder的append方法,没有加synchronized,效率要高

  • 测试,for循环1000万次,分别使用StringBuffer和StringBuilder进行append方法

    • 加了锁消除,相差10%~20%左右,相差不大
    • 关闭锁消除,因为锁消除默认是开启的,所以增加虚拟机参数,-XX:-EliminateLocks 关闭锁消除,此时性能相差四五倍,差别很大

标量替换

  • 逃逸分析
    • 虚拟机优化技术中,有一种比较激进的手段,如果一个对象不会被方法体外面访问,真实场景中可以不去堆中分配,可以走栈上分配,逃逸分析就是分析出来这个对象不会被外部访问,如果这个对象可以拆,这个对象里面的字段,可以用标量替换
实例
  • package ex16;
    
    /**
     * @author King老师
     * 标量替换
     * <p>
     * -XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
     * -XX:-DoEscapeAnalysis 关闭逃逸分析
     * <p>
     * -XX:+EliminateAllocations开启标量替换(jdk1.8默认开启)
     * -XX:-EliminateAllocations 关闭标量替换
     */
    public class VariableDemo {
    
        public void foo() {
            Teacher teacher = new Teacher();
            teacher.name = "king";
            teacher.age = 18;
            //to do something
        }
    
        public void foo1() {
            String name = "king";
            int age = 18;
            //to do something
        }
    
    }
    
    class Teacher {
        String name;
        String sexType;
        int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getSexType() {
            return sexType;
        }
    
        public void setSexType(String sexType) {
            this.sexType = sexType;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    
    
  • 如果开启逃逸分析和标量替换,foo方法会被优化成foo1方法

    • 如果逃逸分析Teacher这个对象不会逃出这个方法,别的方法没有调用,所以可以走栈上分配,同时发现调用这个对象只是用了它的age和name这些字段,对象是多余的,所以会去掉这个对象

    • jdk1.8中逃逸分析和标量替换是默认开启的

Java的历史发展和未来展望

Java的发展历史

  • 1995 年 5 月 23 日,Sun 公司正式发布了 Java 语言和 HotJava 浏览器;
  • 1996 年 1 月,Sun 公司发布了 Java 的第一个开发工具包(JDK 1.0);
  • 1996 年 4 月,10 个最主要的操作系统供应商申明将在其产品中嵌入 Java 技术,发展可真是迅雷不及掩耳;
  • 1996 年 9 月,大约 8.3 万个网页应用了 Java 技术来制作,这就是早年的互联网,即 Java Applet,真香定律;
  • 1996 年 10 月,Sun 公司发布了 Java 平台第一个即时编译器(JIT),这一年很不平凡;
  • 1997 年 2 月 18 日,JDK 1.1 面世,在随后的三周时间里,达到了 22 万次的下载量,PHP 甘拜下风;
  • 1999 年 6 月,Sun 公司发布了第二代 Java 三大版本,即 J2SE、J2ME、J2EE,随之 Java2 版本发布;
  • 2000 年 5 月 8 日,JDK 1.3 发布,四年升三版,不算过分哈;
  • 2000 年 5 月 29 日,JDK 1.4 发布,获得 Apple 公司 Mac OS 的工业标准支持;
  • 2001 年 9 月 24 日,Java EE 1.3 发布,注意是 EE,从此开始臃肿无比;
  • 2002 年 2 月 26 日,J2SE 1.4 发布,自此 Java 的计算能力有了大幅度的提升,与 J2SE 1.3 相比,多了近 62% 的类与接口;
  • 2004 年 9 月 30 日 18:00PM,J2SE 1.5 发布,1.5 正式更名为 Java SE 5.0;
  • 2005 年 6 月,在 JavaOne 大会上,Sun 公司发布了 Java SE 6;
  • 2009 年 4 月 20 日,Oracle 宣布收购 Sun,该交易的总价值约为 74 亿美元;
  • 2010 年 Java 编程语言的创始人 James Gosling 从 Oracle 公司辞职,一朝天子一朝臣,国外也不例外;
  • 2011 年 7 月 28 日,Oracle 公司终于发布了 Java 7,这次版本升级经过了将近 5 年时间;
  • 2014 年 3 月 18 日,Oracle 公司发布了 Java 8,这次版本升级为 Java 带来了全新的 Lambda 表达式。

JDK的发展

Java 7
  • try、catch 能够捕获多个异常
  • 新增 try-with-resources 语法
  • JSR341 脚本语言新规范
  • JSR203 更多的 NIO 相关函数
  • JSR292,课程中提到的 InvokeDynamic
  • 支持 JDBC 4.1 规范
  • 文件操作的 Path 接口、DirectoryStream、Files、WatchService
  • jcmd 命令
  • 多线程 fork/join 框架
  • Java Mission Control
Java 8
  • 支持 Lamda 表达式
  • 支持集合的 stream 操作
  • 提升了 HashMaps 的性能(红黑树)
  • 提供了一系列线程安全的日期处理类
  • 完全去掉了 Perm 区
Java 9
  • JSR376 Java 平台模块系统
  • JEP261 模块系统
  • jlink 精简 JDK 大小
  • G1 成为默认垃圾回收器
  • CMS 垃圾回收器进入废弃倒计时
  • GC Log 参数完全改变,且不兼容
  • JEP110 支持 HTTP2,同时改进 HttpClient 的 API,支持异步模式
  • jshell 支持类似于 Python 的交互式模式
Java 10
  • JEP304 垃圾回收器接口代码进行整改
  • JEP307 G1 在 FullGC 时采用并行收集方式
  • JEP313 移除 javah 命令
  • JEP317 重磅 JIT 编译器 Graal 进入实验阶段
Java 11
  • JEP318 引入了 Epsilon 垃圾回收器(这个回收器什么都不干,适合短期任务)
  • JEP320 移除了 JavaEE 和 CORBA Modules,应该要走轻量级路线
  • Flight Recorder 功能,类似 JMC 工具里的功能
  • JEP321 内置 httpclient 功能,java.net.http 包
  • JEP323 允许 lambda 表达式使用 var 变量
  • 废弃了 -XX+AggressiveOpts 选项
  • 引入了 ZGC,依然是实验性质
Java 12
  • JEP189 先加入 ShenandoahGC
  • JEP325 switch 可以使用表达式
  • JEP344 优化 G1 达成预定目标
  • 优化 ZGC
Java 13
  • JEP354 yield 替代 break
  • JEP355 加入了 Text Blocks,类似 Python 的多行文本
  • ZGC 的最大 heap 大小增大到 16TB
  • 废弃 rmic Tool 并准备移除
Java 14
  • JEP343 打包工具引入
  • JEP345 实现了 NUMA-aware 的内存分配,以提升 G1 在大型机器上的性能
  • JEP359 引入了 preview 版本的 record 类型,可用于替换 lombok 的部分功能
  • JEP364 之前的 ZGC 只能在 Linux 上使用,现在 Mac 和 Windows 上也能使用 ZGC 了
  • JEP363 正式移除 CMS,CMS 涉及到的一些优化参数,在 14 版本普及之后,将不复存在
总结
  • 每一个版本的发布,Java 都会对以下进行改进:

    • 优化垃圾回收器,减少停顿,提高吞吐
    • 语言语法层面的升级
    • 结构调整,减少运行环境的大小,模块化
    • 废弃掉一些承诺要废弃的模块
  • Java 9 之后,已经进入了快速发布阶段,大约每半年发布一次,Java 8 和 和 Java 11 的 是目前支持的 LTS 版本(Long Term Support :长期演进版)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值