小师妹学JVM之逃逸分析和TLAB

40次阅读

共计 3099 个字符,预计需要花费 8 分钟才能阅读完成。

简介

逃逸分析我们在 JDK14 中 JVM 的性能优化一文中已经讲过了,逃逸分析的结果就是 JVM 会在栈上分配对象,从而提升效率。如果我们在多线程的环境中,如何提升内存的分配效率呢?快来跟小师妹一起学习 TLAB 技术吧。

逃逸分析和栈上分配

小师妹:F 师兄,从前大家都说对象是在堆中分配的,然后我就信了。上次你居然说可以在栈上分配对象,这个实在是颠覆了我一贯的认知啊。

柏拉图说过:思想永远是宇宙的统治者。只要思想不滑坡,办法总比困难多。别人告诉你的都是一些最基本,最最通用的情况。而师兄我告诉你的则是在优化中的特列情况。

小师妹:F 师兄,看起来 JVM 在提升运行速度方面真的做了不少优化呀。

是呀,Java 从最开始被诟病速度慢,到现在执行速度直追 C 语言。这些运行时优化是必不可少的。还记得我们之前讲的逃逸分析是怎么回事吗?

小师妹:F 师兄,这个我知道,如果一个对象的分配是在方法内部,并且没有多线程访问的情况下,那么这个对象其实可以看做是一个本地对象,这样的对象不管创建在哪里都只对本线程中的本方法可见,因此可以直接分配在栈空间中。

对的,栈上分配的对象因为不用考虑同步,所以执行速度肯定会更加快速,这也是为什么 JVM 会引入栈上分配的原因。

再举一个形象直观的例子。工厂要组装一辆汽车,在 buildCar 的过程中,需要先创建一个 Car 对象,然后给它按上轮子。

  public static void main(String[] args) {buildCar();
  }
  public static void buildCar() {Wheel whell = new Wheel(); // 分配轮子
    Car car = new Car(); // 分配车子
    car.setWheel(whell);
  }
}

class Wheel {}

class Car {
  private Wheel whell;
  public void setWheel(Wheel whell) {this.whell = whell;}
}

考虑一下上面的情况,如果假设该车间是一个机器人组装一台车,那么上面方法中创建的 Car 和 Wheel 对象,其实只会被这一个机器人访问,其他的机器人根本就不会用到这个车的对象。那么这个对象本质上是对其他机器人隐形的。所以我们可以不在公共空间分配这个对象,而是在私人的栈空间中分配。

逃逸分析还有一个作用就是 lock coarsening。同样的,单线程环境中,锁也是不需要的,也可以优化掉。

TLAB 简介

小师妹:F 师兄,我觉得逃逸分析很好呀,栈上分配也不错。既然又这么厉害的两项技术了,为什么还要用到 TLAB 呢?

首先这是两个不同的概念,TLAB 的全称是 Thread-Local Allocation Buffers。Thread-Local 大家都知道吧,就是线程的本地变量。而 TLAB 则是线程的本地分配空间。

逃逸分析和栈上分配只是争对于单线程环境来说的,如果在多线程环境中,不可避免的会有多个线程同时在堆空间中分配对象的情况。

这种情况下如何处理才能提升性能呢?

小师妹:哇,多个线程竞争共享资源,这不是一个典型的锁和同步的问题吗?

锁和同步是为了保证整个资源一次只能被一个线程访问,我们现在的情况是要在资源中为线程划分一定的区域。这种操作并不需要完全的同步,因为 heap 空间够大,我们可以在这个空间中划分出一块一块的小区域,为每个线程都分一块。这样不就解决了同步的问题了吗?这也可以称作空间换时间。

TLAB 详解

小师妹,还记得 heap 分代技术中的一个中心两个基本点吗?哦,1 个 Eden Space 和 2 个 Suvivor Space 吗?

Young Gen 被划分为 1 个 Eden Space 和 2 个 Suvivor Space。当对象刚刚被创建的时候,是放在 Eden space。垃圾回收的时候,会扫描 Eden Space 和一个 Suvivor Space。如果在垃圾回收的时候发现 Eden Space 中的对象仍然有效,则会将其复制到另外一个 Suvivor Space。

就这样不断的扫描,最后经过多次扫描发现任然有效的对象会被放入 Old Gen 表示其生命周期比较长,可以减少垃圾回收时间。

因为 TLAB 关注的是新分配的对象,所以 TLAB 是被分配在 Eden 区间的,从图上可以看到 TLAB 是一个一个的连续空间。

然后将这些连续的空间分配个各个线程使用。

因为每一个线程都有自己的独立空间,所以这里不涉及到同步的概念。默认情况下 TLAB 是开启的,你可以通过:

-XX:-UseTLAB

来关闭它。

设置 TLAB 空间的大小

小师妹,F 师兄,这个 TLAB 的大小是系统默认的吗?我们可以手动控制它的大小吗?

要解决这个问题,我们还得去看 JVM 的 C ++ 实现,也就是 threadLocalAllocBuffer.cpp:

上面的代码可以看到,如果设置了 TLAB(默认是 0),那么 TLAB 的大小是定义的 TLABSize 除以 HeapWordSize 和 max_size() 中最小的那个。

HeapWordSize 是 heap 中一个字的大小,我猜它 =8。别问我为什么,其实我也是猜的,有人知道答案的话可以留言告诉我。

TLAB 的大小可以通过:

-XX:TLABSize

来设置。

如果没有设置 TLAB,那么 TLAB 的大小就是分配线程的平均值。

TLAB 的最小值可以通过:

-XX:MinTLABSize

来设置。

默认情况下:

-XX:ResizeTLAB

resize 开关是默认开启的,那么 JVM 可以对 TLAB 空间大小进行调整。

TLAB 中大对象的分配

小师妹:F 师兄,我想到了一个问题,既然 TLAB 是有大小的,如果一个线程中定义了一个非常大的对象,TLAB 放不下了,该怎么办呢?

好问题,这种情况下又有两种可能性,我们假设现在的 TLAB 的大小是 100K:

第一种可能性:

目前 TLAB 被使用了 20K,还剩 80K 的大小,这时候我们创建了一个 90K 大小的对象,现在 90K 大小的对象放不进去 TLAB,这时候需要直接在 heap 空间去分配这个对象,这种操作实际上是一种退化操作,官方叫做 slow allocation。

第二中个可能性:

目前 TLAB 被使用了 90K,还剩 10K 大小,这时候我们创建了一个 15K 大小的对象。

这个时候就要考虑一下是否仍然进行 slow allocation 操作。

因为 TLAB 差不多已经用完了,为了保证后面 new 出来的对象仍然可以有一个 TLAB 可用,这时候 JVM 可以尝试将现在的 TLAB Retire 掉,然后分配一个新的 TLAB 空间,把 15K 的对象放进去。

JVM 有个开关,叫做:

-XX:TLABWasteTargetPercent=N

这个开关的默认值是 1。表示如果新分配的对象大小如果超出了设置的这个百分百,那么就会执行 slow allocation。否则就会分配一个新的 TLAB 空间。

同时 JVM 还定义了一个开关:

-XX:TLABWasteIncrement=N

为了防止过多的 slow allocation,JVM 定义了这个开关(默认值是 4),比如说第一次 slow allocation 的极限值是 1%,那么下一次 slow allocation 的极限值就是 %1+4%=5%。

TLAB 空间中的浪费

小师妹:F 师兄,如果新分配的 TLAB 空间,那么老的 TLAB 中没有使用的空间该怎么办呢?

这个叫做 TLAB Waste。因为不会再在老的 TLAB 空间中分配对象了,所以剩余的空间就浪费了。

总结

本文介绍了逃逸分析和 TLAB 的使用。希望大家能够喜欢。

本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/jvm-escapse-tlab/

本文来源:flydean 的博客

欢迎关注我的公众号: 程序那些事,更多精彩等着您!

正文完
 0