对象已死?
垃圾收集器在对堆进行垃圾回收时, 第一件事情就是确定这些对象那些还存活着, 那些已经死去。
引用计数算法: 每当有一个引用, 计数器值就加一,反之减一。 任何时刻计数器为零的对象时不可能在被使用的, 效率高, 原理简单。(java并没有采用)
问题: 不能解决循环依赖问题, 实际上a和b是不能被访问了, 但是应为相互依赖, 导致不能被垃圾回收。
可达性分析算法:
基本思路通过一系列的 GC Roots 的根对象作为起始节点集, 根据引用关系, 向下收缩, 走过的路径称为引用链, 如果某个对象到GC ROOTS间没用任何引用链, 或者用图论来说就是这个对象不可达, 那么证明此对象不能再被使用。
固定可做为GC Roots对象可包括:
1. 虚拟机栈中引用对象,比如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量
2. 方法区中静态属性引用的对象
3. 方法区中常量引用对象, 比如字符串常量池里的引用
4. 本地方法栈的引用的对象
5. 虚拟机内部的引用,如基本数据类型对应的引用对象,常驻的异常类, 系统类加载器
6. 所有被同步锁持有的对象
除了固定的, 我们也可以根据所选的垃圾收集器和当前回收内存区域,对其他对象可以临时性加入===> 分代收集, 局部回收
垃圾收集算法分为 【引用计数式垃圾收集】 和 【追踪式垃圾收集】*
分代收集理论:
1. 弱分代假说 ===> 朝生夕死 年轻代
2. 强分代假说 ====> 熬过很多次都不死, 老年代
根据上述俩个假说是垃圾收集器设计的原则, 如果有一部分区域专门处理朝生夕死, 一部份区域处理很难消亡的对象, 这样就可以实现在年轻我标记少量的存活对象, 而老年代则可以换一种方式, 从而实现比较低代价回收大量的空间。 对于老年代虚拟机可以采用较低的频率来回收这块区域, 这就同时兼顾垃圾收集的时间开销和内存的空间有效利用
3. 跨代引用假说
因为在一次年轻代回收后, 并不能确定老年代是否引用了它, 这时就需要在固定的GC Roots中在额外遍历老年代的引用, 确保正确性!
从而有了跨代的假说 ==> 因为存在跨代引用是极少的, 并且在年轻代对象存在跨代引用, 会因为老年代引用着我, 最终导致晋升到老年代, 跨代引用消除。
依据该条假说, 我们不再为了少量的跨代区扫描老年代, 也不必要浪费空间专门记录每个对象是否存在那些跨代引用, 【我们只需要在新生代建立一个全局的数据结构, 记忆集, 这个结构会把老年代分为若干个小块, 标识出那块老年代存在跨代引用。 此后, 只有标记有跨代引用的块才会被加入GC Roots
标记-清除算法
就是标记回收的对象, 统计回收掉所有标记的对象, 也可以标记存活的对象,缺点: 1. 如果有大量的对象需要标记,这必将会影响回收的效率。 第二就是会造成大量的内存碎片
标记-复制算法:
他把可用内存分为大小对等的俩快, 每次只使用其中的一块, 将存活的对象复制到另外一块中。
缺点: 如果存在大量存活的对象将导致大量的复制开销, 其二: 每次都会导致一般的内存空闲, 这是极大的浪费。解决了空间碎片的问题。
标记-整理算法:
针对老年代对象的存亡特征, 有针对性的“标记-整 理” 算 法, 其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活 的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。
安全点:
在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的加持,它带来了相对短暂并且不会随堆的增大而增大, 即稳定的。但是遍历对象图, 则是很困难的,和堆的容量成正比关系。
【标记】阶段是所有追踪式垃圾收集算法的共同特征, 这个阶段会随着堆变大而增加停顿时间。 如果能够消减, 将是系统性的提升。
首先理解为什么对象图需要保证快照一致性下, 才能遍历对象图?
采用三色标记作为辅助工具, 把遍历过的对象按照【是否被访问】的条件标记成三种颜色,