探究跨代引用和逃逸分析如何提升程序性能!

简介: 探究跨代引用和逃逸分析如何提升程序性能!


🍊 跨代引用

在Java虚拟机中,跨代引用指的是老年代中的对象引用了年轻代中的对象。当进行年轻代的垃圾回收时,需要扫描老年代中所有引用年轻代的对象,在老年代里面做大量的遍历和扫描操作,这是非常消耗性能的。为了优化这个问题,JVM引入了记忆集(Card Table)的抽象数据结构。

记忆集记录了从非收集区域指向收集区域的一个指针集合,这个指针集合是由一组连续的内存卡片(Card)组成的。每个内存卡片的大小是2^18个字节,可以理解为一个内存页面。记忆集能够记录指向年轻代的内存卡片中所有的指针,将这些指针标记为"dirty"。在minor gc时,只需要扫描"dirty"的内存卡片,而不是扫描老年代中所有引用年轻代的对象。

思路比较清晰,现在来看一下这个问题的示例代码:

public class CrossGenerationReferenceExample {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        Object obj = new Object();
        for (int i = 0; i < 1000000; i++) {
            list.add(obj);
        }
    }
}

这个示例代码中的问题在于,对象obj在年轻代中创建,但是被放到了老年代中的ArrayList里面。这个时候,当进行年轻代的垃圾回收时,就需要扫描老年代中所有引用年轻代的对象,而且这个ArrayList还是一个非常大的对象,扫描的效率非常低下。

现在来看一下使用记忆集优化之后的代码:

public class CrossGenerationReferenceExample {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        Object obj = new Object();
        for (int i = 0; i < 1000000; i++) {
            list.add(obj);
            if (i % 20000 == 0) {
                System.gc();
            }
        }
    }
}

在这个示例代码中,每添加20000个对象就手动执行一次垃圾回收操作。这个操作会强制触发记忆集的更新,即将内存卡片中的指针标记为"dirty"。这样,在下一次垃圾回收时,就只需要扫描"dirty"的内存卡片,提高了垃圾回收的效率。

🍊 逃逸分析

逃逸分析是Java虚拟机优化的重要手段之一,它可以通过静态分析来确定对象的动态作用域,判断一个对象是否会“逃逸”,即被其他方法或线程引用。如果对象不会逃逸,则可以将对象的创建和销毁都放在栈上进行,从而减少在堆上进行内存分配和回收的开销。

逃逸分析有三种程度:从不逃逸、方法逃逸和线程逃逸。这三个由低到高表示不同逃逸的程度。

🎉 栈上分配

在Java虚拟机中,堆中的对象对于所有线程都是可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾回收子系统会回收堆中不再使用的对象,但是回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。

如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁,垃圾回收子系统的压力将会下降很多。

栈上分配可以支持方法逃逸,但不能支持线程逃逸。

下面是一个栈上分配的示例代码:

public static void main(String[] args) {
    for (int i = 0; i < 1000000; i++) {
        Point p = new Point(1, 2);
        p.setX(i);
        p.setY(i);
    }
}
class Point {
    private int x;
    private int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public void setX(int x) {
        this.x = x;
    }
    public void setY(int y) {
        this.y = y;
    }
}

在这个示例代码中,每次循环都创建一个Point对象,在Point对象的构造函数中将xy初始化。Point对象不会逃逸出方法之外,因此可以将它们分配在栈上。

🎉 标量替换

一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。

一个数据可以继续分解,那它就被称为聚合量。Java中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。

假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。

将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

下面是一个标量替换的示例代码:

public static void main(String[] args) {
    for (int i = 0; i < 1000000; i++) {
        String s = new String("abc" + i);
        int length = s.length();
    }
}

在这个示例代码中,每次循环都创建一个String对象,并取出它的长度。逃逸分析能够证明String对象不会逃逸出方法之外,因此可以将对象的成员变量拆分为一个char数组和一个offset整型变量。在Java虚拟机内部,会把char数组和offset整型变量分别分配在栈上,而不需要在堆上分配String对象。

总之,逃逸分析是一种非常重要的JVM优化手段,能够让JVM能够更好地利用现代计算机的硬件资源,提高程序的性能。通过栈上分配和标量替换等技术,能够在一定程度上减少对堆内存的使用,从而让垃圾回收的效率更高,进而让程序能够更好地使用计算机的内存和CPU资源。


相关文章
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
49 6
|
2月前
|
算法 Java 编译器
你为什么不应该过度关注go语言的逃逸分析
【10月更文挑战第21天】逃逸分析是 Go 语言编译器的一项功能,用于确定变量的内存分配位置。变量在栈上分配时,函数返回后内存自动回收;在堆上分配时,则需垃圾回收管理。编译器会根据变量的使用情况自动进行逃逸分析。然而,过度关注逃逸分析可能导致开发效率降低、代码复杂度增加,并且对性能的影响相对较小。编译器优化通常比人工干预更准确,因此开发者应更多关注业务逻辑和整体性能优化。
|
4月前
|
存储 编译器 Go
Go语言中的逃逸分析
Go语言中的逃逸分析
|
7月前
|
存储 缓存 Java
深入理解Java中的逃逸分析
深入理解Java中的逃逸分析
116 0
|
6月前
|
Java 程序员
理解强引用:在编程中的作用和风险
理解强引用:在编程中的作用和风险
52 0
|
7月前
|
安全 编译器 C#
C#中的可空引用类型:减少空引用异常的利器
【1月更文挑战第9天】C# 8.0中引入的可空引用类型特性,它通过在编译时提供更精确的静态分析,帮助开发者减少运行时的空引用异常。文章详细阐述了可空引用类型的工作原理、如何配置项目以使用此特性,以及在实际编码中如何利用可空引用类型提升代码的健壮性和可读性。
|
7月前
|
缓存 算法 JavaScript
提高Java程序性能!了解可达性分析算法、强软弱虚引用和三色标记GC的过程,避免不可达对象阻碍程序性能!
提高Java程序性能!了解可达性分析算法、强软弱虚引用和三色标记GC的过程,避免不可达对象阻碍程序性能!
146 0
|
测试技术 编译器 C++
栈局部变量优化探究,意外发现了 vs 的一个 bug ?
栈局部变量优化探究,意外发现了 vs 的一个 bug ?
|
Java 编译器
JVM(三)逃逸分析
JVM(三)逃逸分析
65 0
|
存储 缓存 算法
jvm之逃逸分析解读
jvm之逃逸分析解读