垃圾收集器与内存分配策略

对象已死?

垃圾收集器在对堆进行垃圾回收时, 第一件事情就是确定这些对象那些还存活着, 那些已经死去。

引用计数算法: 每当有一个引用, 计数器值就加一,反之减一。 任何时刻计数器为零的对象时不可能在被使用的, 效率高, 原理简单。(java并没有采用)

问题: 不能解决循环依赖问题, 实际上a和b是不能被访问了, 但是应为相互依赖, 导致不能被垃圾回收。

可达性分析算法:

基本思路通过一系列的 GC Roots 的根对象作为起始节点集, 根据引用关系, 向下收缩, 走过的路径称为引用链, 如果某个对象到GC ROOTS间没用任何引用链, 或者用图论来说就是这个对象不可达, 那么证明此对象不能再被使用。

固定可做为GC Roots对象可包括:

1. 虚拟机栈中引用对象,比如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量

2. 方法区中静态属性引用的对象

3. 方法区中常量引用对象, 比如字符串常量池里的引用

4. 本地方法栈的引用的对象

5. 虚拟机内部的引用,如基本数据类型对应的引用对象,常驻的异常类, 系统类加载器

6. 所有被同步锁持有的对象

除了固定的, 我们也可以根据所选的垃圾收集器和当前回收内存区域,对其他对象可以临时性加入===> 分代收集, 局部回收

垃圾收集算法分为 【引用计数式垃圾收集】 和 【追踪式垃圾收集】*

分代收集理论:

1. 弱分代假说 ===> 朝生夕死 年轻代

2. 强分代假说 ====> 熬过很多次都不死, 老年代

根据上述俩个假说是垃圾收集器设计的原则, 如果有一部分区域专门处理朝生夕死, 一部份区域处理很难消亡的对象, 这样就可以实现在年轻我标记少量的存活对象, 而老年代则可以换一种方式, 从而实现比较低代价回收大量的空间。 对于老年代虚拟机可以采用较低的频率来回收这块区域, 这就同时兼顾垃圾收集的时间开销和内存的空间有效利用

3. 跨代引用假说

因为在一次年轻代回收后, 并不能确定老年代是否引用了它, 这时就需要在固定的GC Roots中在额外遍历老年代的引用, 确保正确性!

从而有了跨代的假说 ==> 因为存在跨代引用是极少的, 并且在年轻代对象存在跨代引用, 会因为老年代引用着我, 最终导致晋升到老年代, 跨代引用消除。  

依据该条假说, 我们不再为了少量的跨代区扫描老年代, 也不必要浪费空间专门记录每个对象是否存在那些跨代引用,  【我们只需要在新生代建立一个全局的数据结构, 记忆集, 这个结构会把老年代分为若干个小块, 标识出那块老年代存在跨代引用。 此后, 只有标记有跨代引用的块才会被加入GC Roots

标记-清除算法

就是标记回收的对象, 统计回收掉所有标记的对象, 也可以标记存活的对象,缺点: 1. 如果有大量的对象需要标记,这必将会影响回收的效率。 第二就是会造成大量的内存碎片

标记-复制算法:

他把可用内存分为大小对等的俩快, 每次只使用其中的一块, 将存活的对象复制到另外一块中。

缺点: 如果存在大量存活的对象将导致大量的复制开销, 其二: 每次都会导致一般的内存空闲, 这是极大的浪费。解决了空间碎片的问题。

标记-整理算法:

针对老年代对象的存亡特征, 有针对性的“标记-整 理 算 法, 其中的标记过程仍然与“标记-清除算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活 的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。

标记 - 清除算法与标记 - 整理算法的本质差异在于前者是一种非移动式的回收算法, 而后者是移动式的。
HotSpot的算法细节实现:
迄今为止, 所有的收集器在根节点枚举时都是必须暂停用户线程的, 因此根节点的枚举和空间内存碎片整理, 都需要【STW】。 虽然可达性分析算法耗时最长的【查找引用链】 已经可用与用户程序并发执行。 但是根节点枚举必须要正 一致性快照, 就是在根节点枚举时, 不能产生新的分析或者对象引用关系, 否则无法保证准确性。
但是好在虚拟机不需要一个不漏的检查完所有执行上下文和全局的引用位置, 采用【OopMap】的数据结构来达到直接获取那些地方存放对象的引用。 一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来, 在即是编译的过程中, 也会在特定的位置记录下栈里和寄存器里那些位置的引用, 这样收集器在扫描的时候就可以直接得知这些消息, 并不需要真正的一个不漏从方法去等GC Roots开始查找

安全点:

在OopMap的协助下, HotSpot可用快速的准确地完成GC Roots枚举, 但是会出现一个问题, 如果为每条指令都生成对应的OopMap, 那将会需要大量额外存储空间, 实际上也的确没用这样做, 利用安全点解决了这个问题, 实现, 不是在任何地方都可以垃圾回收, 必须到达安全点, 而安全点的特征就是指令序列复用, 如【方法调用,循环跳转, 异常跳转】等都是属于指令复用, 所以这些地方都会产生安全点。

 其二: 如何在垃圾收集时, 让所有的线程都跑到最近的安全点, 然后停顿下来。 这里有两种方案: 1. 【抢先式中断】2. 【主动式抢断】

而我们都是主动式中断, 不会直接对线程操作, 而是i简单设置了一个标志位, 各个线程去轮询这个标志, 一旦发现中断为真时, 就会在安全点主动中断。 轮询标志和安全点重合, 另外创建对象和需要在java堆中分配内存的地方也有, 为了检测是否即将要发生垃圾收集,避免没用足够内存分配新对象。

由于轮询操作会平凡出现, 这要求必须高效, HotSpot使用内存保护陷阱的方式, 把轮询操作精简为只有一条指令的程度【test】

安全区域:
安全点保证了程序执行时, 在不太长的时间内就会遇到可进入垃圾收集过程的安全点。 但是,程序“不执行”的时候了? 就是没用分配处理器时间, 处于sleep || Blocked 状态, 这时候线程无法响应中断, 不能走到安全的地方中断自己。 对于这种情况就有了安全区域来解决。

安全区域指确保在某一段代码片段中, 引用关系不会发生变化, 因此, 在这个区域中任意地方垃圾收集都是安全的。 拉伸后的安全点, 当用户线程到达安全区域后, 首先会进行标识, 那么在这段时间内虚拟机发起垃圾收集时就不需要去管这些申明自己在安全区域的线程。 但是当要离开安全区时, 它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集时需要暂停用户线程), 如果完成, 当什么都没发生, 反之阻塞

记忆集和卡表

为了解决对象跨代引用所带来的问题, 垃圾收集器在新生代建立了名为记忆集的数据结构, 用于避免吧整个老年代加进GC Roots扫描范围。 事实上不是只有分代区域中会有这个问题, 而是所有设计区域收集的垃圾收集器, 都面临一样的问题。

无需考虑跨代指针的细节, 采用粗犷的记录粒度来节省记忆集的存储和维护成本,记录精度的三种方式:1.【字节精度】2.【对象精度】3.【卡精度】 都包含跨代指针

目前常用的卡精度==> 【卡表】实现了抽象的数据结构记忆集, 可用比作成, HashMap来理解卡表。

字节数组CARD_TABLE [this address >> 9]的每一个元素都对应与其标识的内存区域中一块特定大小的内存块, 这个内存块叫做卡页。一个卡页的内存中通常包含不止一个对象, 只有卡页内用一个或多个对象字段存在跨代指针, 那就将对应的卡表数组元素的值标识为1, 称为元素变脏。最后筛选可得跨代指针,加入GC Roots

写屏障:

我们采用了记忆集缩减GC Roots扫描范围的问题, 但还没有解决卡表元素如何维护的问题, 列如他们何时变脏, 谁把他们变脏。

何时变脏一般很明确, 发生在引用类型字段赋值的那一刻, 但如何变脏, 变脏时如何跟新维护卡表, 如果时解释执行,虚拟机负责每条指令的执行, 有大部分介入空间。 但是编译执行后纯粹为机器指令流, 这就必须要机器码层次的手段, 把维护卡表的动作放在每一个赋值操作之中。

在HotSpot虚拟机是通过写屏障实现的【并不是CPU优化指令顺序的内存屏障】, 写屏障面对引用类型赋值时, 采用【AOP】思想对引用赋值对象产生环形通知, 供程序执行额外的操作。

产生缺点:每次赋值前都需要跟新卡表的操作, 但是这个开销比扫描整个GC Roots是低很多的。

其二就是【伪共享】, 就是现代中央处理器的缓存系统是以缓存行为单位, 当多线程修改互相独立的变量时, 恰好共享一个缓存行, 就会彼此影响【同步,无效化,写回】等问题导致性能降低。 为了避免伪共享, 只有当卡表元素未被标记时才将其标记未脏。增加了额外的判断,可通【-XX:+UserCondCardMark】参数来决定是否开启。

并发的可达性分析:

可达性分析算法理论上要求全过程必须都基于能保障一致性的快照才能够分析, , 这意味着必须冻结用户线程的运行。 在根节点枚举上,由于GC Roots相比整个堆,还是算比较少, 在加上OopMap的加持,它带来了相对短暂并且不会随堆的增大而增大, 即稳定的。但是遍历对象图, 则是很困难的,和堆的容量成正比关系。

【标记】阶段是所有追踪式垃圾收集算法的共同特征, 这个阶段会随着堆变大而增加停顿时间。 如果能够消减, 将是系统性的提升。

首先理解为什么对象图需要保证快照一致性下, 才能遍历对象图?
采用三色标记作为辅助工具, 把遍历过的对象按照【是否被访问】的条件标记成三种颜色,

白色 :尚未访问过。分析结束都还是白色,即被回收
黑色 :本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰色 :本对象已访问过,但是本对象 引用到 的其他对象尚未全部访问完。全部访问后,会转换为黑色。
当用户线程与收集器并发工作时, 同时修改了对象图将会导致俩种后果:
1. 把原本消亡的对象标记存活, 就是本来黑色A ==> 灰色B, 但是这时又执行了黑色A ==> null
这个时候灰色B应该失去了引用, 但是它已经是灰色了, 所有本轮GC不会回收灰色B, 这被称为【浮动垃圾】
2. 漏标:黑色A ==> 灰色E ===> 白色G ,  如果灰色E==白色G被断开, 这时黑色A ==> 白色G。 而白色G失去了灰色E的引用不会变成灰色|黑色,造成本应该是黑色的对象被错标未白色, 在垃圾回收时被清理, 这个时候问题就很严重, 因为本应该是A在引用我, 出现了对象丢失问题。
赋值器插入一条或多条黑色对象到白色对象的引用
赋值器删除了全部灰色对象到该白色对象的直接或间接引用
因此, 我们解决并发扫描问题时对象消失问题, 只需要破坏这个俩个条件的任意一个即可
1. 【增量更新】
        当黑对象新指向白色对象引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中黑色对象为更, 再一次扫描即可。 即一旦黑色指向白色, 白色将变成灰色
2. 【原始快照】
        灰色对象删除引用白色对象的关系时, 就将这个删除引用记录下来, 即并发扫描完后, 在将这些记录关系灰色对象为根, 重新扫描一次。 无论引用关系删除与否,都会按照刚才开始扫描的那一刻的对象图进行搜索。
无论引用关系记录还是删除, 虚拟机记录操作都是通过写屏障实现的, 在HotSpot虚拟机中,增量更新为CMS基于增量更新来做并发标记    原始快照: G1 Shenandoah
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值