• 07-JVM内存逃逸分析与实践


    简介

    背景

    随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

    在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

    逃逸分析技术到现在还不是很成熟,虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

    何为逃逸分析

    逃逸分析一种数据分析算法,基于此算法可以有效减少Java对象在堆内存中的分配。Hotspot虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
    1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
    2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。

    逃逸分析案例演示

    逃逸对象

    如下代码中的StringBuffer发生了逃逸,不会在栈上分配。

    public StringBuffer append(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以调整为如下写法,例如:

    public StringBuffer append(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    未逃逸对象

    当一个对象在方法内创建,又没有被外界引用,此对象为为逃逸对象。例如:

    public void create(int x,int y) {
        Point p1= new Point(x,y);
        //…
        p1=null;
       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    逃逸分析参数设置

    在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析,如果使用的是较早的版本,开发人员则可以通过:

    1)选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析。
    2)通过选项“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果。

    建议:开发中能在方法内部应用对象的,就尽量控制在内部。

    代码优化实践

    概述

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

    1)栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引用不会发生逃逸,对象可能是栈上分配的候选。

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

    3) 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    栈上分配

    /**
     * 栈上分配测试(-XX:-DoEscapeAnalysis)
     * -Xmx128m -Xms128m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
     */
    public class ObjectStackAllocationTests {
        public static void main(String[] args) throws InterruptedException {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                alloc();
            }
            long end = System.currentTimeMillis();
            System.out.println("花费的时间为: " + (end - start) + " ms");
            // 为了方便查看堆内存中对象个数,线程sleep
            TimeUnit.MINUTES.sleep(5);
        }
        private static void alloc() {
            byte[] data = new byte[10];//未发生逃逸
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    对如上代码运行测试时,分别开启和关闭逃逸分析,检查控制台日志的输出以及花费时间上的不同。

    同步锁消除

    我们知道线程同步是靠牺牲性能来保证数据的正确性,这个过程的代价会非常高。程序的并发行和性能都会降低。JVM的JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程应用?假如是,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码上加的锁。这个取消同步的过程就叫同步省略,也叫锁消除。例如:

    public class SynchronizedLockTest {
        public void lock() {
            Object obj= new Object();
            synchronized(obj) {
                System.out.println(obj);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    标量替换分析

    所谓的标量(scalar)一般指的是一个无法再分解成更小数据的数据。例如,Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象分解成若干个变量来代替。这个过程就是标量替换。例如:

    package com.java.jvm;
    /**
     * 标量替换测试 (-XX:+EliminateAllocations)
     * -Xmx128m -Xms128m -XX:+DoEscapeAnalysis 
    -XX:+PrintGC -XX:-EliminateAllocations 
     */
    public class ObjectScalarReplaceTests {
    
        public static void main(String args[]) {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                alloc();
            }
            long end = System.currentTimeMillis();
            System.out.println("花费的时间为: " + (end - start) + " ms");
        }
    
        private static void alloc() {
            Point point = new Point(1,2);
        }
    
        static class Point {
            private int x;
            private int y;
            public Point(int x,int y){
                this.x=x;
                this.y=y;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    对于上面代码,假如开启了标量替换,那么alloc方法的内容就会变为如下形式:

    private static void alloc() {
              int x=10;
    int y=20;
        }
    
    • 1
    • 2
    • 3
    • 4

    alloc方法内部的Point对象是一个聚合量,这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换为栈上分配提供了很好的基础。

    小节面试分析

    1)什么是逃逸分析?
    2)逃逸分析有什么优势、劣势?
    3)什么是栈上分配,为什么要栈上分配?
    4)什么是锁消除,何时会触发锁消除?
    5)什么是标量替换,标量替换可以解决什么问题?
    6)Hotspot虚拟机中所有对象都是分配在堆中吗?(是的)
    7HotSpot虚拟机中对象的栈上分配如何理解?(本质上是标量替换)

  • 相关阅读:
    python数据分析及可视化(十三)pyecharts可视化(简介、特性、全局配置项、图形的绘制、多图布局)
    springboot疫情防控学生自助申报系统毕业设计源码260839
    vue——计算属性、侦听属性、组件、组件通信、ref属性、数据总线、动态组件、插槽
    Spring Cloud之声明式服务调用(Feign)
    Python爬虫之Js逆向案例(15)-XX话题批量获取&保存到CSV文件
    5、超链接标签
    Linux——MySQL安装的几种方式
    每天一个设计模式之访问者模式(Visitor Pattern)
    gitlab本地备份(自动定时备份)
    STL教程3-异常机制
  • 原文地址:https://blog.csdn.net/maitian_2008/article/details/124913715