JAVA的垃圾收集器与内存分配策略【一篇文章直接看懂】( 三 )


故收集器在扫描时,可以直接查到引用信息,不需要从方法区/GC ROOT查找
安全点
解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题,安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点
并没有为每条指令生成,只是在“特定位置”记录这些信息,成为安全点() 。
GC强制要求必须执行到安全点后才能执行
抢先式(没人用了):在GC发生时中断所有用户线程,若线程不在安全点上,则恢复执行,重新中断知道跑到安全点上
主动式:GC需要中断线程时,不直接对线程操作,设置一个标志位,线程执行时主动地轮询该标志,为真时,到附近的安全点挂起 。标志位置与安全点是重合的 。
安全区域
对于处于Sleep/状态的线程,解决无法响应虚拟机的中断请求 。由此引入安全区域(Safe )来解决 。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的 。我们也可以把安全区域看作被扩展拉伸了的安全点 。
执行到安全区域的代码时
标识自己已经进入了安全区域,故GC发生时,不再管理这些线程离开安全区域时,检查是否虚拟机完成了根节点枚举,若完成则继续执行,若没完成,则等待直到接收到信号 记忆集与卡表
记忆集( Set):解决对象跨代引用的问题,记录从非收集区域指向收集区域的指针集合的抽象数据结构
最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现
但是对此方案,维护与空间成本很高,但收集器只需要通过记忆集判断是否存在某指针,所以可以使用更粗的粒度进行记录 。
第三种的卡精度也成为“卡表”,是最常用的记忆集实现形式 。卡表定义了记忆集的记录精度、与堆内存的映射关系等 。其中的每个元素对应着一块特定大小内存块,成为卡页(Card Page),(大小通常为 2 N 2^N 2N的字节数)
在卡页中不只一个对象,只要有一个存在跨代指针,则将对应的元素值标记为1,称为元素变脏(Dirty),没有则表示为0 。
GC发生->筛选脏元素->得出存在跨代指针的内存块->放入GC Root中一并扫描
写屏障
为了解决卡表元素如何维护的问题,如:何时变脏、谁把他们变脏
有其他分代区域中对象引用了本区域的对象时变脏在机器码层面中,使用写屏障,把维护卡表的动作放到每个赋值操作之中
解释执行的字节码,VM执行每条字节码指令,而在编译执行中,代码是纯粹的机器指令流了
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形()通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内 。
这边我认为AOP切面,就是与中的权限验证功能类似,在执行时判断是否可以执行 。将我们原本一条线执行的程序在中间切开加入了一些其他操作一样 。
在赋值前的部分为:前屏障(Pre-Write ),后的为:后屏障(Post-Write )
引用写屏障后->为所有赋值操作生成指令->写屏障增加更新卡表操作
存在伪共享(False )问题:
因为CPU的缓存系统是以缓存行(Cache Line)为单位的,当多线程修改互相独立的变量,且变量共享同一行,会彼此影响(写回、无效化、同步)->降低了性能
解决方法:1.先检查未被标记过才标记为脏 2.JDK7新增了参数,但是增加了一次判断的开销
并发的可达性分析
包含“标记”阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器,因此并行收益是极大的