JVM源码分析之线程局部缓存TLAB

转自:http://www.kejixun.com/article/170523/330012.shtml


介绍TLAB之前先思考一个问题:

  创建对象时,需要在堆上申请指定大小的内存,如果同时有大量线程申请内存的话,可以通过锁机制或者指针碰撞的方式确保不会申请到同一块内存,在JVM运行中,内存分配是一个极其频繁的动作,这种方式势必会降低性能。

  因此,在Hotspot 1.6的实现中引入了TLAB技术。

  什么是TLAB

  TLAB全称ThreadLocalAllocBuffer,是线程的一块私有内存,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用,这个申请动作还是需要原子操作的。

  TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,均摊对GC堆(eden区)里共享的分配指针做更新而带来的同步开销。

  TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

  TLAB实现

  实现位于/Users/zhanjun/openjdk/hotspot/src/share/vm/memory/threadLocalAllocBuffer.hpp

  

  TLAB简单来说本质上就是三个指针:start,top 和 end,每个线程都会从Eden分配一大块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

  而 top 就是里面的分配指针,一开始指向跟 start 同样的位置,然后逐渐分配,直到再要分配下一个对象就会撞上 end 的时候就会触发一次 TLAB refill,refill过程后续会解释。

  _desired_size 是指TLAB的内存大小。

  _refill_waste_limit 是指最大的浪费空间,假设为5KB,通俗一点讲就是:

  1、假如当前TLAB已经分配96KB,还剩下4KB,但是现在new了一个对象需要6KB的空间,显然TLAB的内存不够了,这时可以简单的重新申请一个TLAB,原先的TLAB交给Eden管理,这时只浪费4KB的空间,在_refill_waste_limit 之内。

  2、假如当前TLAB已经分配90KB,还剩下10KB,现在new了一个对象需要11KB,显然TLAB的内存不够了,这时就不能简单的抛弃当前TLAB,这11KB会被安排到Eden区进行申请。

  在Java代码中执行new Thread()的时候,会触发以下代码

  

  JavaThread的run方法中,第一步就是调用this->initialize_tlab();方法初始化TLAB,initialize_tlab实现如下:

  

  其中tlab()返回的就是一个ThreadLocalAllocBuffer对象,调用initialize()初始化TLAB,实现如下:

  

  1、设置当前TLAB的_desired_size,该值通过initial_desired_size()方法计算;

  2、设置当前TLAB的_refill_waste_limit,该值通过initial_refill_waste_limit()方法计算;

  3、初始化一些统计字段,如_number_of_refills、_fast_refill_waste、_slow_refill_waste、_gc_waste和_slow_allocations;

  字段_desired_size的计算过程分析

  

  TLABSize在argument模块中默认会设置大小为 256 * K,也可以通过JVM参数选择进行设置,不过即使设置了也会和一个最大值max_size进行比较,然后取一个较小值,其中max_size计算如下:

  

  这里明确说明了TLAB的大小不能超过可以容纳 int[Integer.MAX_VALUE],有点疑惑,why?

  字段_refill_waste_limit计算分析

  计算逻辑很简单,其中TLABRefillWasteFraction默认 64

  内存分配

  new一个对象,假设需要1K的大小,我们一步一步看看是如何分配的。

  

  对象的内存分配入口为instanceKlass::allocate_instance(),通过CollectedHeap::obj_allocate()方法在堆内存上进行分配

  

  其中common_mem_allocate_init()方法最终会调用CollectedHeap::common_mem_allocate_noinit()方法,实现如下:

  

  根据UseTLAB的值,决定是否在TLAB上进行内存分配,如果JVM参数中没有手动取消UseTLAB,会调用allocate_from_tlab()在TLAB上尝试分配,因为可能存在分配失败的情况,比如TLAB容量不足,看下allocate_from_tlab()的实现:

  

  从上述实现可以看出,先会尝试调用ThreadLocalAllocBuffer 的 allocate 方法,如果返回为空,再执行allocate_from_tlab_slow()进行分配,从这个方法命名可以看出这是比较慢的分配路径。

  ThreadLocalAllocBuffer 的 allocate 方法实现如下:

  

  通过判断当前TLAB的剩余容量是否大于需要分配的大小,来决定分配结果,如果当前剩余容量不够,就返回NULL,表示分配失败。

  慢分配allocate_from_tlab_slow()实现如下:

  

  1、如果当前TLAB的剩余容量大于浪费阈值,就不在当前TLAB分配,直接在共享的Eden区进行分配,并且记录慢分配的内存大小;

  2、如果剩余容量小于浪费阈值,说明可以丢弃当前TLAB了;

  3、通过allocate_new_tlab()方法,从eden新分配一块裸的空间出来(这一步可能会失败),如果失败说明eden没有足够空间来分配这个新TLAB,就会触发YGC。

  申请好新的TLAB内存之后,执行TLAB的fill()方法,实现如下:

  

  包括下述几个动作:

  1、统计refill的次数

  2、初始化重新申请到的内存块

  3、将当前TLAB抛弃(retire)掉,这个过程中最重要的动作是将TLAB末尾尚未分配给Java对象的空间(浪费掉的空间)分配成一个假的“filler object”(目前是用int[]作为filler object)。这是为了保持GC堆可以线性parse(heap parseability)用的。


展开阅读全文

没有更多推荐了,返回首页