HotSpot的算法细节

读周志明老师《深入理解java虚拟机》笔记

1.根节点枚举

  1. 固定可作为GC roots的节点主要在全局性的引用,(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),查找需要耗费不少时间。
  2. 所有收集器在根节点枚举这一步都必须暂停用户线程。类似标记整理算法。

2.安全点

  1. 在OopMap协助下,HotSpot可以快速准确地完成GCroot的枚举,但有问题,如果为每条指令都生成对应的Oop那会需要大量额外的存储空间。
  2. 世界上HotSpot虚拟机没有为每条指令生成Oop他只为特定位置记录信息,这些位置被称为安全点。强制到达安全点后才能停。
  3. 安全点的选定既不能太少,让垃圾收集器等待时间太长,也不能太多频繁收集。安全点的选定基本基于是否具有让程序长时间的运行的特征。
  4. 长时间执行最明显就是指针序列的复用如:方法调用,循环跳转。
  5. 如何在垃圾收集时让所有线程都到达安全点(1)抢先式中断(2)主动式中断

3.安全区域

程序不执行时,没有分配时间。线程无法响应虚拟机的中断请求,不能再走到安全的地方去挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间,这是需要安全区域。

安全区域是指能够确保在某一段代码片段中,引用关系不会发生变化。因此在这个地方任何时间进行垃圾收集都是安全的。

用户线程执行到安全区域后会自动标识自己已经进入安全区域。虚拟机发生垃圾收集时就不必去管那些声明自己在安全区域的线程。当线程要离开安全区域后是,他要检查虚拟机是否完成了根节点枚举,如果完成了,就当做没有事发生,继续执行。否则必须一直等待,直到枚举完,收到信号为止。

4.记忆集和卡表

记忆集解决跨代引用的抽象数据机构。避免把整个老年代加入GC Root扫描范围。所有的部分区域收集行为的垃圾收集器都会面临的问题。

收集器只需要通过记忆集判断某一块非收集区域是否存在有指向收集区域的指针就可以了。

记录精度:

  • 字长精度
  • 对象精度
  • 卡精度

卡精度即卡表最常用的实现形式。是记忆集的一种实现形式。

卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的。以下这行代码是HotSpot默认的卡表标记逻辑:

CARD_TABLE [this address >> 9] = 0;
1

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块,如下图。
在这里插入图片描述

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

5.写屏障

解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。但问题是如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?假如是解释执行的字节码,那相对好处理,虚拟机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢?经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。下面这段代码是一段更新卡表状态的简化逻辑:

void oop_field_store(oop* field, oop new_value) {
	// 引用字段赋值操作    
	*field = new_value;    
	// 写后屏障,在这里完成卡表状态更新    
	post_write_barrier(field, new_value); 
}
123456

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。

伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示:

if (CARD_TABLE [this address >> 9] != 0)    
	CARD_TABLE [this address >> 9] = 0;
12

在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

6.并发得可达性分析

三色标记法:

  • 白色:表示对象尚未被垃圾收集器访问过
  • 黑色:对象已经被垃圾收集器扫描过,且这个对象的所有引用都已经扫描过。
  • 灰色:表示对象已经被垃圾收集器扫描过,但这个对象至少存在一个引用还没有被扫描过。

当且仅当以下两个条件同时满足时,会产生“对象”消失的问题,即本应是黑色的对象被误标为白色。

  • 赋值器插入一条或多条从黑色对象到白色对象的新引用。
  • 赋值器删除全部从灰色对象到该白色对象的直接或间接引用。

解决并发扫描时对象消失问题只需破坏两个条件的任意一个即可

  • 增量更新
  • 原始快照

增量更新:黑色对象一旦插入指向白色对象的引用之后,他就变成灰色对象

原始快照:无论关系删除与否,都会按照刚刚开始扫描的那一刻的对象快照来进行搜索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值