• JIT内存逃逸分析


    什么是JIT

    JIT Compiler(Just-in-timeCompiler) 即时编译。
    最早的Java建置方案是由一套转译程式(interpreter),将每个Java指令都转译成对等的微处理器指令,并根据转译后的指令先后次序依序执行,由于一个Java指令可能被转译成十几或数十几个对等的微处理器指令,这种模式执行的速度相当缓慢。
    详细解析见百度百科 JIT编译器

    JIT内存逃逸分析

    Escape Analysis 内存逃逸分析是一种代码分析手段,通过动态分析创建对象的使用范围。

    1. 内存逃逸分类

    如果一个方法创建的对象,除了方法内使用到之外,还被方法外使用到,那么在方法执行结束后,由于该对象依然被引用到,那么GC就可能无法立即回收,此时该对象就出现了逃逸。
    内存逃逸分为方法逃逸和线程逃逸

    • 方法逃逸
      当一个对象被定义后,以参数的形式传递给其它方法
    • 线程逃逸
      类变量或者示例变量或者方法返回的对象,可能被其它线程引用或者访问到
    public class EscapeAnalysis {
    
        public static Object obj1;
    
        public Object obj2;
    
        public void globalVariableEscape() {
            obj1 = new Object();  // 静态变量,外部线程可见,会发生逃逸
        }
    
        public void instanceObjectEscape() {
            obj2 = new Object();  // 赋值给堆中实例字段,外部线程可见,会发生逃逸
        }
    
        public Object returnObjectEscape() {
            return new Object();   // 返回实例,外部线程可见,会发生逃逸
        }
    
        public void noEscape() {
            Object noEscape = new Object();   // 仅创建线程可见,对象无逃逸
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2. 逃逸分析的作用

    筛选出没有发生逃逸的对象,为其优化手段,例如栈上分配,标量替换,同步消除等提供依据。
    只有server模式下才能启用逃逸分析
    在这里插入图片描述

    JVM相关参数

    • 开启逃逸分析:-XX:+DoEscapeAnalysis
    • 关闭逃逸分析:-XX:-DoEscapeAnalysis
    • 显示逃逸分析:-XX:+PrintEscapeAnalysis

    3. 同步锁消除

    同步锁时非常消耗性能的,所以编译器确定一个对象没有发生逃逸时,它会移除该对象的同步锁。
    JDK1.8 默认开启了同步锁,但是建立在开启逃逸分析的基础上。

    -XX:+EliminateLocks #开启同步锁消除(JVM默认状态)
    -XX:-EliminateLocks #关闭同步锁消除
    
    • 1
    • 2

    我们用一个例子来演示

        @Test
        public void testLock(){
            long t1 = System.currentTimeMillis();
            for (int i = 0; i < 100_000_000; i++) {
                locketMethod();
            }
            long t2 = System.currentTimeMillis();
            System.out.println("耗时:"+(t2-t1));
        }
    
        public static void locketMethod(){
            EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
            synchronized(escapeAnalysis) {
                escapeAnalysis.obj2="abcdefg";
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面这个EscapeAnalysis没有发生逃逸,JVM默认开启了同步锁消除。我们做几组试验对比
    (1) 手动注释掉synchronized锁,直接运行,未设置JVM参数
    在这里插入图片描述
    (2) 保留synchronized锁,直接运行,未设置JVM参数
    在这里插入图片描述
    (3) 设置JVM参数,关闭锁消除,再次运行

    java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
    
    • 1

    在这里插入图片描述
    在这里插入图片描述
    (4) 设置JVM参数,开启锁消除,再次运行

    java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks
    
    • 1

    在这里插入图片描述
    通过上述4组参照试验对比,可以得出,JDK1.8默认开启了锁消除,如果关闭锁消除,那么锁将非常消耗性能。

    4. 标量替换

    标量:不能被进一步分解的变量,如基础数据类型和对象的引用。
    聚合量:能够被分解的变量,比如对象。

    如果一个对象没有发生逃逸,可以将成员变量拆分成标量,就可以不用创建它,在栈或者寄存器中直接创建成员标量,这就叫标量替换。节省内存,提升了应用程序的性能。
    JDK1.8默认开启了标量替换,也同样建立在逃逸分析的基础上

    JVM相关参数

    • 开启标量替换:-XX:+EliminateAllocations #JVM默认状态
    • 关闭标量替换:-XX:-EliminateAllocations
    • 显示逃逸分析:-XX:+PrintEliminateAllocations
     @Test
        public void testScalarReplace(){
            long t1 = System.currentTimeMillis();
            for (int i = 0; i < 100_000_000; i++) {
                scalarReplace();
            }
            long t2 = System.currentTimeMillis();
            System.out.println("耗时:"+(t2-t1));
        }
    
        public static void scalarReplace(){
            User user=new User();
            user.setId(1);
            user.setName("hello");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上面这个User没有发生逃逸,JVM默认开启了标量替换。我们做几组试验对比。
    (1) 直接运行,未设置JVM参数
    在这里插入图片描述
    (2) 设置JVM参数,关闭标量替换

    java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateAllocations
    
    • 1

    在这里插入图片描述

    (3) 设置JVM参数,开启标量替换

    java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
    
    • 1

    在这里插入图片描述

    通过上述3组参照试验对比,可以得出,JDK1.8默认开启了标量替换,如果关闭标量替换,那么直接进行对象分配将非常影响性能。

    5. 栈上分配

    将原本应当分配在堆上的对象分配到栈内存上,这样可以减少堆内存的使用,从而减少GC的频率。

    1. Java对象的分配流程

    首先,我们需要先了解决Java对象是如何分配内存的。
    在这里插入图片描述

    • 如果开启栈上分配,JVM会先进行栈上分配
    • 如果没有开启栈上分配或不符合条件,则会进入TLAB分配
    • 如果TLAB分配不成功或者不符合,则判断是否可以进入年老代分配
    • 如果不能进入年老代,则进入eden分配
      并不是所有对象都分配在堆上,除了堆意外,还可以将对象分配到栈和TLAB中。(大多数的对象都分配在堆中)

    2. 栈上分配思路

    栈上分配是JVM提供的一项优化技术。
    其思路是:

    • 对于线程私有的对象(不能被其它线程访问的对象),可以将它们分配到栈内存上,而不是堆内存中,也就是将聚变量进行标量替换的方案。
    • 分配到栈上的优势是可以在方法结束后自动销毁,不需要GC介入,提供系统性能
    • 对于大量零散的对象,栈上分配提供了一种很好的对象分配策略,栈上分配速度块,可以有效避免GC回收带来的负面影响。
    • 问题:由于栈内存比较小,因而大对象不能也不适合进行栈上分配。

    3. 开启栈上分配

    栈上分配是基于逃逸分析和标量替换的,所以必须开启逃逸分析和标量替换,当然JDK1.8是默认都是开启的。
    逃逸分析和标量替换前面已经介绍了。
    在这里插入图片描述

    4. 分析内存

    栈上分配时基于逃逸分析+标量替换,因此我们只要关闭逃逸分析或者标量替换任一一项,即可关闭栈上分配。
    我们继续使用前面标量替换的demo。

    • 关闭栈上分配,运行测试用例
    java -Xmx15m -Xms15m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB -XX:-DoEscapeAnalysis #关闭栈上分配
    
    • 1

    在这里插入图片描述

    • 开启栈上分配,运行测试用例
    java -Xmx15m -Xms15m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB -XX:+DoEscapeAnalysis #开启栈上分配
    
    • 1

    在这里插入图片描述
    通过对比,我们可以看到

    • 开启栈上分配,将未逃逸的对象分配在栈内存中,明显运行效率更高。
    • 关闭栈上分配后,GC频繁进行垃圾回收。

    6. 查看JVM所有配置的参数值

    java -XX:+PrintFlagsFinal  #输出打印所有参数jvm参数
    
    • 1

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    力扣203 - 移除链表元素【LeetCode转VS调试技巧教学】
    ubuntu ffmpeg 合成字幕 字体缺失selecting one more font for
    GP09|公司赚的多,股票涨的好?
    异步为什么会造成 HTTP 队首阻塞?
    Python小游戏——小鸟管道游戏【含完整源码】
    Qt 定制专属闹钟
    广东桉木建筑模板:天然美触,打造高品质建筑
    认识时间复杂度和简单排序算法
    JS(JavaScript)
    Vue——计算属性(计算属性简介、计算属性和方法的区别:(面试)、关于计算属性 函数什么情况下调用、案例)
  • 原文地址:https://blog.csdn.net/u011628753/article/details/126666483