对象分配概述
-
G1提供了两种分配策略
- 基于线程本地分配缓冲区(Thread Local Allocation Buffer,TLAB)的快速分配
- 基于TLAB的慢速分配
- 慢速分配
- 当不能成功分配对象时就会触发垃圾回收
对象分配流程图
线程本地分配缓冲区(Thread Local Allocation Buffer,TLAB)
快速分配
- TLAB的产生就是为了快速分配内存
- JVM堆是所有线程的共享区域,所以,从JVM堆空间分配对象时,必须锁定整个堆,以便不会被其他线程中断和影响。
- TLAB就是为每个线程都分配的一个独立的缓冲区,这样就可以减少使用锁的频率,只有在为每个线程分配TLAB的时候,才需要锁定整个JVM堆
- TLAB属于Eden区域中的内存,不同线程的TLAB都位于Eden区,Eden区对所有的线程都是可见的。每个线程的TLAB有内存区间,在分配的时候只在这个区间分配。
- JVM分配TLAB的时候用CAS分配
-
JVM快速分配TLAB对象流程
- 从线程的TLAB分配空间,如果成功则返回。
- 如果分配失败,则尝试先分配一个新的TLAB,再分配对象
- 如果TLAB过小,那么TLAB不能储存更多的对象,可能需要不断地给整个堆上锁重新分配新的TLAB。
- 如果TLAB过大,那么容易导致更多的内存碎片,内存使用效率不高。更容易发生GC。
- JVM提供了参数 TLABSize来控制TLAB的大小,默认为0,JVM会自动推断这个值多大合适。
-
参数TLABWasteTargetPercent,用于设置TLAB占Eden空间的百分比默认值1%
- 推断方式:TLABSize = Eden 2 1% / 线程个数(乘以2是假设内存使用服从均匀分布)
指针碰撞法分配
- 如果TLAB剩余空间(end - top) 大于当前对象待分配空间。则直接修改 top = top + objSize(对象大小);
- 如果TLAB满了,则会保留这一部分空间,重新从堆内存中划一片空间给TLAB
如何判断TLAB满了
- 虚拟机内部会维护一个 refill_waste,当请求对象大于refill_waste,会选择在堆中分配,若小于该值,废弃当前的TLAB,新建一个TLAB来分配该对象。
- refill_waste 可以使用 TLABRefillWasteFraction 参数来调整,默认值为64,即表示使用1/64的TLAB空间作为refill_waste
- 假设 TLAB为1M,那 refill_waste 为16k,即当TLAB使用了1008k时(1024k - 16k),就直接分配一个新的,否则就尽量使用这个老的TLAB
如何调整TLAB
- 除了 TLABRefillWasteFraction ,JVM还提供了参数TLAB WasteIncrement(默认值为4个字),用于动态增加refill_waste
- refill_waste和TLAB的大小都会不断动态调整,使系统状态达到最优。
- 由于大对象都不会在新生代中,TLAB都不能分配大对象,所以TLAB的大小不会大于HRSize/2。(大于HRSize/2就会被认为是大对象)
TLAB中的慢速分配
- 如果TLAB中的剩余空间很小(TLAB满了),说明这个空间通常不满足对象分配,可以直接丢弃,填充一个dummy对象,然后申请一个新的TLAB来分配对象。
- 如果TLAB剩余空间比较多,那就不能丢弃TLAB,这时候就直接将对象分配到堆中,不使用TLAB,直接返回。
清理老的TLAB(dummy对象填充)
- GC在线性扫描堆的时候(比如查看HeapRegion对象,并行标记等),要知道哪里有对象,哪里是空白的。对于对象,扫描之后,可以直接跳过这个对象的长度,如果是空白的,就需要一个字一个字的扫,效率很低。所以把TLAB的空白地方分配一个dummy对象(哑元对象),这样GC就能做到快速遍历了。
申请一个新的TLAB缓冲区
调用G1CollectHeap中分配,主要是在attempt_allocation中完成
- 快速无锁分配:在当前可以分配的堆空间内,通过CAS来获取一块内存,如果成功,就可以作为TLAB的空间。
- 因为使用CAS,所以也有可能不成功。
- 不成功则进行慢速分配。
-
慢速分配需要尝试对Heap加锁,拓展新生代区域或垃圾回收等处理后再分配。
- 首先尝试对堆分区进行加锁后进行分配,成功则返回。
- 如果不成功,则判定是否可以对新生代分区进行拓展。如果可以拓展,则拓展以后再分配TLAB,成功则返回。
- 如果不成功,则判定是否可以进行垃圾回收,如果可以进行,垃圾回收以后再分配,成功则返回。
- 如果不成功,则再次尝试,如果尝试次数达到阈值(默认2),则返回失败(NULL)。(如果可以继续尝试,则从快速分配开始重头尝试)
日志及解读
通过命令设置参数,如下所示:
-Xmx128M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-XX:+PrintTLAB -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest
可以得到:
garbage-first heap total 131072K, used 37569K [0x00000000f8000000,
0x00000000f8100400, 0x0000000100000000)
region size 1024K, 24 young (24576K), 0 survivors (0K)
TLAB: gc thread: 0x0000000059ade800 [id: 16540] desired_size: 491KB slow
allocs: 8 ref?ill waste: 7864B alloc: 0.99999 24576KB ref?ills: 50
waste 0.0% gc: 0B slow: 816B fast: 0Bd
分析日志中TLAB这个信息的每一个字段含义:
- desired_size:期望分配的TLAB的大小,这个值就是我们前面提到如何计算TLABSize的方式。在这个例子中,第一次的时候,不知道会有多少线程,所以初始化为1,desired_size=24576/50 = 491.5KB这个值是经过取整的。
- slow allocs:发生慢速分配的次数,日志中显示有8次分配到heap而没有使用TLAB。
- refill waste:retire一个TLAB的阈值。
- alloc:该线程在堆分区分配的比例。
- refills:发生的次数,这里是50,表示从上一次GC到这次GC期间,一共retire过50个TLAB块,在每一个TLAB块retire的时候都会做一次ref?ill把尚未使用的内存填充为dummy对象。
-
waste:由3个部分组成:
- gc:发生GC时还没有使用的TLAB的空间。
- slow:产生新的TLAB时,旧的TLAB浪费的空间,这里就是新生成50个TLAB,浪费了816个字节。
- fast:指的是在C1中,发生TLAB retire(产生新的TLAB)时,旧的TLAB浪费的空间。
慢速分配
这里的慢速分配指的是在TLAB中分配,不能成功,最后进入慢速分配。(比TLAB慢速分配更慢
- attempt_allocation尝试进行对象分配,如果成功则返回。
- 如果大对象,在attempt_allocatin_humongous分配,直接分配老年代.
- 如果分配不成功,则进行GC垃圾回收(主要是FullGC),然后再分配。
- 最终成功,或者尝试N次后失败,则分配失败。
大对象分配流程(和TLAB类似,唯一区别就是对象大小不同)
- 尝试垃圾回收(主要是增量回收,同时启动并发标记)
-
尝试开始分配对象
- 如果大于HRSize的一半且小于HRSize(即一个完整分区可以保存):直接从空闲列表获得一个分区,或者分配一个新分区。
- 如果是大于HRSize,则是个连续对象,需要多个分区,思路同上,但是需要加锁。
- 如果失败再从尝试垃圾回收开始。如果失败达到一定次数,则分配失败。
最后的分配尝试
- 尝试拓展新的分区,成功则返回
- 不成功进行则进行FullGC,但是不回收软引用,再次分配成功则返回
- 不成功再次进行FullGC,回收软引用,成功则返回
- 不成功则返回Null,分配失败
G1垃圾回收的时机
- 分配内存时发现内存不足
-
外部显式调用
-
java代码中的system.gc()
- 如果设置了DisableExplicitGC(默认为false),则不接受这个函数显式触发GC
- 默认为FullGC,如果设置了ExplicitGCInvokesConcurrent,表示可以进行并发的混合回收
-
和JNI交互,JNI代码进入了临界区
- 如JNI代码为了优化性能,提供了一个函数jni_GetPrimitiveArrayCritical/jni_GetStringCritical用于直接访问原始内存数据,但是为了保证安全必须使用GCLocker进行加锁。当加锁后发生了GC请求,此时GC会被延迟,直到GCLocker执行了unlock会重新补一个GC
- 如果设置了ExplicitGCInvokesConcurrent,表示可以进行并发的混合回收,如果没有设置,可能启动新生代回收
-
参数介绍和调优
- 在优化调试TLAB的时候,在调试环境中可以通过打开PrintTLAB来观察TLAB分配和使用的情况。
- 参数UseTLAB,指是否使用TLAB。大量的实验可以证明使用TLAB能够加速对象分配;该参数默认是打开的,不要关闭它。
- 参数ResizeTLAB,指是否允许TLAB大小动态调整。前面提到TLAB会进行动态化调整,主要是基于历史信息(分配大小、线程数等),有基准测试表明使用动态调整TLAB大小效率更高。
- 参数MinTLABSize,指设置TLAB的最小值。实际应用需要设置该值,比如64K,一般可以根据情况设置和调整该值。
- 参数TLABSize,指设置TLAB的大小。实际中不要设置TLABSize,设置之后TLAB就不能动态调整了,即会使用一个固定大小的TLAB,前面我们提到GC可以根据情况动态调整TLAB,在分配效率和内存碎片之间找到一个平衡点,如果设置该值则这种平衡就失效了。
- 参数TLABWasteTargetPercent,指的是TLAB可占用的Eden空间的百分比,默认值是1。可以根据情况调整TLABWasteTargetPercent,增大则可以分配更多的TLAB,3.1节中给出了具体的计算方式;另外如果实际中线程数目很多,建议增大该值,这样每个线程的TLAB不至于太小。
- 参数TLABRefillWasteFraction,指的是TLAB中浪费空间和TLAB块的比例,默认值是64。可以根据情况调整TLABRef?illWasteFraction,主要考量点是内存碎片和分配效率的平衡,如果发现日志waste中的slow和fast很大,说明浪费严重,可以适当减少该参数值。
- 参数TLABWasteIncrement,指的是动态的增加浪费空间的字节数,默认值是4。增加该值会增加TLAB浪费的空间;一般不用设置。
- 参数GCLockerRetryAllocationCount默认值为2,表示当分配中的垃圾回收次数超过这个阈值之后则直接失败。
TLAB不是G1才引入的,对象分配是JVM提供的基础分配功能,只不过G1结合自己内存分区的特征,以及垃圾回收的具体实现,重新实现了分配的策略,重用了这些参数的功能和使用方法,且没有引入额外的参数,所以这一部分内容不仅适用于G1的调优,其他的垃圾回收器同样适用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。