3

面试官:Java中对象都存放在堆中吗?你知道逃逸分析? - 万猫学社

 2 years ago
source link: https://www.cnblogs.com/heihaozi/p/16003365.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

面试官:Java中对象都存放在堆中吗?你知道逃逸分析?

面试官:Java虚拟机的内存分为哪几个区域?

我(微笑着):程序计数器、虚拟机栈、本地方法栈、堆、方法区

面试官:对象一般存放在哪个区域?

面试官:对象都存放在堆中吗?

我:是的。

面试官:你了解过逃逸分析吗?

我(皱了皱眉):是内存溢出吗?

面试官:不是的。

我(挠了挠头):不是很了解。

面试官:今天的面试先到这,回去等消息吧!

然后就没有然后了,不甘心的我开始了查找相关资料。

逃逸分析(Escape Analysis)是一种确定对象的引用动态范围的分析方法,说人话就是:分析在程序的哪些地方可以访问到对象的引用。

当一个对象在方法中被分配时,该对象的引用可能逃逸到其它执行线程中,或是返回到方法的调用者。

如果一个方法中分配一个对象并返回一个该对象的引用针,那么该对象可能被访问到的地方就无法确定,此时对象的引用就发生了“逃逸”。
如果对象的引用存储在静态变量或者其它数据结构中,因为静态变量是可以在当前方法之外访问到,此时对象的引用也发生了“逃逸”。

逃逸分析确定某个对象的引用可以被访问的所有地方,以及确定能否保证对象的引用的生命周期只在当前进程或线程中。

对象的逃逸状态一般分为三种:全局逃逸、参数逃逸、没有逃逸。

全局逃逸(GlobalEscape)

对象的引用逃出了方法或者线程。比如:对象的引用赋值给了一个静态变量,或者存储在一个已经逃逸的对象中, 或者对象的引用作为方法的返回值给了调用方法。

比如饿汉的单例模式:

package one.more;

public final class GlobalEscape {

    // instance对象赋值给了一个静态变量,发生了全局逃逸
    private static GlobalEscape instance = new GlobalEscape();

    private GlobalEscape() {
    }

    public static GlobalEscape getInstance() {
        return instance;
    }
}

参数逃逸(ArgEscape)

对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸。这个状态是通过分析被调用方法的字节码来确定的。

package one.more;

public class ArgEscape {

    class Rectangle {

        private int length;
        private int width;

        public Rectangle(int length, int width) {
            this.length = length;
            this.width = width;
        }

        public int getArea() {
            return this.length * this.width;
        }
    }

    public int getArea(int length, int width) {
        Rectangle rectangle = buildRectangle(length, width);
        return rectangle.getArea();
    }

    private Rectangle buildRectangle(int length, int width){
        Rectangle rectangle = new Rectangle(length, width);
        // rectangle对象发生了参数逃逸
        return rectangle;
    }
}

没有逃逸(NoEscape)

方法中的对象没有发生逃逸,这意味着可以不将该对象分配在堆上。

package one.more;

public class NoEscape {

    class Rectangle {

        private int length;
        private int width;

        public Rectangle(int length, int width) {
            this.length = length;
            this.width = width;
        }

        public int getArea() {
            return this.length * this.width;
        }
    }

    public int getArea(int length, int width) {
        // rectangle对象没有逃逸
        Rectangle rectangle = new Rectangle(length, width);
        return rectangle.getArea();
    }
}

逃逸分析后的优化

如果一个对象没有发生逃逸,或者只有参数逃逸,就可能为这个对象采取不同程度的优化,比如:栈上分配、标量替换、同步消除。

栈上分配(Stack Allocations)

如果一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
那么,对象就会随着方法的结束而自动销毁了,可以降低垃圾收集器运行的频率,垃圾收集的压力就会下降很多。

标量替换(Scalar Replacement)

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java虚拟机中的基本数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java中的对象就是典型的聚合量。

如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为基本类型来访问,这个过程就称为标量替换

如果一个对象没有发生逃逸,可以进行标量替换,那么对象的成员变量就在栈上分配和读写,不需要分配到堆中。

标量替换可以视作栈上分配的一种特例,实现更简单,但对逃逸程度的要求更高,它不允许对象没有发生逃逸。

同步消除(Synchronization Elimination)

线程同步本身是一个相对耗时的过程,如果一个对象没有逃逸出线程,无法被其他线程访问,那么该对象的读写肯定就不会有竞争,对该对象实施的同步加锁操作也就可以安全地消除掉。

说了这么多,可以发现对象并不是都在堆上分配内存的。因为通过逃逸分析后,可以对没有逃逸的对象进行标量替换。

另外,由于复杂度等原因,HotSpot中目前还不支持栈上分配的优化。

最后,谢谢你这么帅,还给我点赞关注


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK