JVM中TLAB(Thread Local Allocation Buffer)+逃逸分析

1、为什么有TLAB(Thread Local Allocation Buffer)

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的

  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

总结一句话:堆上存放的对象实例多线程不安全,并且影响分配速度。如果能将对象实例分配在独立的一块区域,那就能解决多线程不安全和分配速度慢问题,这就是TLAB。

2、什么是TLAB

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

  • 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。

3、TLAB使用

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。

  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间(默认是开启的)。

  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项 “-XX:TLABWasteTargetPercent” 设置TLAB空间所占用Eden空间的百分比大小。

  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

4、通过逃逸分析判断对象实例可能被优化成栈上分配

如何将堆上的对象分配到栈,需要使用逃逸分析手段。

在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:

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

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

此外,基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

5、逃逸分析概述

这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

发生逃逸和未发生逃逸实例

public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /**
     * 方法返回EscapeAnalysis对象,发生逃逸
     * @return
     */
    public EscapeAnalysis getInstance() {
        return obj == null ? new EscapeAnalysis() : obj;
    }

    /**
     * 为成员属性赋值,发生逃逸
     */
    public void setObj() {
        this.obj = new EscapeAnalysis();
    }

    /**
     * 对象的作用于仅在当前方法中有效,没有发生逃逸
     */
    public void useEscapeAnalysis() {
        EscapeAnalysis e = new EscapeAnalysis();
    }

    /**
     * 引用成员变量的值,发生逃逸
     */
    public void useEscapeAnalysis2() {
        EscapeAnalysis e = getInstance();
    }
}

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除(这种主动释放内存的方式相比堆内对象等待GC要更好)。

6、逃逸分析设置

参数设置:

在JDK 6u23 版本之后,HotSpot中默认就已经开启了逃逸分析

这个默认大前提是启用了-server模式,当然JVM默认也是-server模式

如果使用的是较早的版本,开发人员则可以通过:

  • 选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析

  • 通过选项“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果

怎么判断是否启用了逃逸分析呢?

  • a、通过jps 查看当前应用进程

  • b、通过jinfo -flag DoEscapeAnalysis 进程号

如果结果是“-XX:-DoEscapeAnalysis”则为未启用,如果是“-XX:+DoEscapeAnalysis”则启用了

7、代码实例比对启用逃逸分析和非逃逸分析

下述代码在主函数中进行了1亿次alloc。调用进行对象创建,User实例的创建作用域未出方法,如果启用逃逸分析,则会启用TLAB。咱们看下启用和不启用逃逸分析,执行时间和堆内对象使用情况。

/**
 * @author liuchao
 * @date 2023/2/24
 */
public class Test {
    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));
        // 方便查看堆内存中对象个数,线程sleep
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }

    public static void alloc() {
        //未发生逃逸
        User user = new User();
    }

}

class User {
    private int age;
    private String userName;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

7.1、启用逃逸分析

默认就启用了,不需要配置,直接看执行结果。

7.2、不启用逃逸分析

配置vm选项:-XX:-DoEscapeAnalysis

查看执行结果

7.3、结论

相同的代码,启用逃逸分析进行1亿次alloc用时7ms,堆上User对象实例占比37%;

不启用逃逸分析进行1亿次alloc用时70ms,堆上User对象实例占比93%;

可以看出相差巨大,所以啊开发中能使用局部变量的,就不要使用在方法外定义。

8、逃逸分析的缺点

关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。

注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。

目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈行动派

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值