万字概览 Java 虚拟机 (2)

标量替换是基于逃逸分析技术的。如果一个对象经过逃逸分析允许被分配在栈上,那么标量替换机制就可以将这个对象的字段直接以局部变量的方式进行分配,而不再采用对象的方法进行分配。

栈上分配自 JDK8 开始是默认开启的,如果关闭了栈上分配或者不符合栈上分配的条件,则 JVM 转而使用 TLAB 机制进行分配。

TLAB 分配

TLAB 是 Thread Local Allocation Buffer 的首字母缩写,代表的是一块线程专属的内存区域。由于我们在 Heap 上无论是使用「指针碰撞」还是「空闲列表」方法进行内存分配,都会遇到多个线程请求内存分配时的同步问题。

TLAB 机制为每一条线程都划分了一块私有的内存区域供其分配对象,当一块 TLAB 被分配满之后就重新分配一块,而原来的那块区域从逻辑上变成了 Heap 的一部分。这样就避免了多线程直接向 Heap 请求内存分配的同步问题,提高了对象分配的效率。一定注意:每一块 TLAB 都是 Heap 的一部分。

从 JVM 源码来看,创建线程的第一步就是分配 TLAB 空间

void JavaThread::run() { // initialize thread-local alloc buffer related fields this->initialize_tlab(); // used to test validitity of stack trace backs this->record_base_of_stack_pointer(); // Record real stack base and size. this->record_stack_base_and_size(); // Initialize thread local storage; set before calling MutexLocker this->initialize_thread_local_storage(); this->create_stack_guard_pages(); this->cache_global_variables(); // ... }

TLAB 的数据结构

class ThreadLocalAllocBuffer: public CHeapObj<mtThread> { friend class VMStructs; private: HeapWord* _start; HeapWord* _top; HeapWord* _end; size_t _desired_size; size_t _refill_waste_limit; // ... }

_start 和 _end 指针分别指向 TLAB 空间的头尾;_top 指针指向当前使用量的边界;_desired_size 是 TLAB 的大小;_refill_waste_limit 是 TLAB 的最大浪费空间。

假设最大浪费空间为 5K,如果 TLAB 目前还剩余 4K 空间,这时需要分配一个 8K 对象,这时就可以选择重新分配一个 TLAB 空间,因为当前这一块 TLAB 放弃后只会浪费 4K 空间,少于阈值。相反,对象则会直接去 Eden 分配,不会舍弃当前 TLAB。

Heap Area

Heap 是 Java 内存模型中最大的一块区域,它被分为 Young Generation 和 Old (Tenured) Generation 两个部分,两个区域的比例默认大约是 1:2。Young 区又按照 8:1:1 的比例分为一个 Eden 区和 2 个 Survivor 区。

万字概览 Java 虚拟机

根据数据统计,程序运行过程中创建的大部分对象都会很快死亡,如果将对象都分配到一整块区域中,那么 GC 在回收这些死亡对象时负担就会变得非常重。为了提高内存的使用效率和 GC 的工作效率,根据对象存活周期的不同,将整个 Heap 划分为了上图中的几个部分。但无论怎么划分,每个区域里面保存的都是分配的对象。

对象的分配过程

当我们需要分配一个对象时,会直接在 Young 区中的 Eden 中尝试分配(大对象会直接在 Old 区上分配,使用参数 PretenureSizeThreshold 控制这个阈值,默认是无限大),如果空间不足则触发一次 YGC;YGC 完成后再次尝试分配,如果还是空间不足,则触发一次 FGC;FGC 结束后依然空间不足以分配,则再次触发 FGC,同时将软引用(Soft Reference)也一并回收;如果还是无法分配,则只能抛出 OOM。

对象在各个区域的流转

对象最初在 Eden 区中进行分配,当 YGC 发生时会将 Young 区所有存活的对象都放到当前没有使用的那个 Survivor 区中,两个 Survivor 区在两次 YGC 之间轮流使用,每次只使用一个(当 YGC 完成后存活对象超过 Survivor 区的大小时,会将部分对象晋升到 Old 区中)。Young 区中的对象每活过一次 YGC,存活周期就加 1,当活过 15 次 YGC 后就会晋升到 Old 区中,直到这个对象不再使用,被 GC 回收掉。

至于为什么需要两个 Survivor 区,我们在后面讲解 GC 算法时再分析。

为什么对象最多活过 15 次 YGC 后就会晋升老年代

因为对象头中记录 YGC 存活周期的字段只有 4 bit 长度,最大表示数字就是 15。但这并不是说对象一定要活过 15 次 YGC 才能晋升到老年代,因为每次 YGC 后都会对这个阈值进行重新计算。比如使用 Serial GC 和 ParNew GC 的情况下,JVM 对于这个阈值的计算逻辑:

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { // TargetSurvivorRatio 默认值是 50 size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; assert(sizes[0] == 0, "no objects with age zero should be recorded"); while (age < table_size) { total += sizes[age]; // check if including objects of age 'age' made us pass the desired // size, if so 'age' is the new threshold if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; if (PrintTenuringDistribution || UsePerfData) { if (PrintTenuringDistribution) { gclog_or_tty->cr(); gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)", desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold); } // .... } return result; }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygjjy.html