1. 对象已死
1.1 引用计数算法
引用计数算法是指添加一个引用计数器,每被引用一次,计数器就加一,当引用失效时,计数器就减一,当计数器为0时就表示该对象不再被使用。
优点:原理简单,效率高。
缺点:不能解决循环引用导致的垃圾回收问题。
1.2 可达性分析算法
可达性分析算法是指以“GC Roots”的根对象作为开始,根据引用关系进行链式关联,形成一条引用链,当某个对象没有被任何一个指向“GC Roots”的引用链关联上,我们就认为该对象已死,可对它进行回收。
可作为GC Roots的对象包括:
- 在虚拟机栈中引用的对象。
- 方法区中类变量引用的对象。
- 方法区中常量池中引用的对象。
- 本地方法栈中JNI(即Native方法)引用的对象。
- 虚拟机中的内部引用。
- 所有被同步锁持有的对象。
- 反映JAVA虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
1.3 引用的特殊用法
用于 描述“食之无味,弃之可惜。”的对象时,对对象进行适当的缓存,JDK从1.2版本后,引入了强引用、弱引用、软引用、虚引用。
1.3.1 强引用
强引用即常规java引用写法,只要引用关系还在,该引用的 对象就不会被回收。
1.3.2 软引用
用SoftReference类来实现,用来描述还有用,但不必须的对象。在系统垃圾回收后还没有足够内存时,会对软引用的对象进行第二次回收。
1.3.3 弱引用
用WeakReference类来实现,比软引用更弱,也是用于描述不必须的对象的,只能生存到下一次垃圾收集发生为止。
1.3.4 虚引用
用PhantomReference类实现,不能通过虚引用获取对象实例,其存在意义是为了在对象被收集器回收时,收到一个系统 通知。
2. 垃圾回收算法
可达性分析算法对应分类是追踪式垃圾收集,也被成为“间接垃圾收集”。
当前商业垃圾收集器大多遵循"分代收集",它建立在三大假说基础上:
- 弱分代假说。绝大多数对象都是朝生熄灭的。
- 强分代假说。熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说。跨代引用相对于同代引用仅占极少数。
2.1 标记-清除算法
标记出所有需要回收或标记所有存活的对象,通过标记结果进行垃圾判断,然后通过垃圾回收期完成最终垃圾回收操作。
它是最基础的垃圾收集算法,它存在两个缺点:
- 执行效率不稳定。标记清除的执行效率跟对象数量密切相关,对象越多,执行效率越低。
- 内存空间的碎片化问题。清除后存在大量不连续的内存碎片,在分配大内存对象时,因无法找到连续且足够大的内存,会触发垃圾收集操作。
2.2 标记-复制算法
面对大量可回收对象时运行效率高。回收新生代大多采用该回收算法,空间碎片化为零,回收后空闲内存具有是连续的,分配对象内存可采用指针碰撞方法。
Appel式回收
Serial、ParNew等新生代收集器采用了该策略,它把新生代分为一块较大的Eden空间和两块较小的Survivor空间,默认Eden和Survivor的大小比例是8:1。
逃生门设计
如果另外一块Survivor空间没有足够空间存放存活对象,将通过分配担保机制直接进入老年代。
2.3 标记-整理算法
针对老年代的对象存活率较高时的垃圾回收算法,它让所有存活的对象往内存空间一端移动,直接清理掉边界以外的内存,它是移动式回收算法。
移动存活对象必须全程暂停用户应用程序,它的优势是解决空间碎片化问题,以及内存空间的分配与访问的高效率。
3. 算法细节实现
3.1 根节点枚举
为了保证分析过程中,根节点集合的对象引用关系不变,在枚举根节点时必须要停顿所有用户线程。
通过使用一组称为OopMap的数据结构,来得到哪些地方存放着对象引用,来避免检查所有执行上线文和全局的引用位置。
3.2 安全点
只有在安全点会生成OopMap,必须执行到安全点后才能够暂停。安全点的选取以“是否具有让程序长时间执行的特征”为标准进行选定,其特征为指令序列的复用,例如方法调用,循环跳转,异常跳转等。
停顿用户线程的方案:强占式中断、主动式中断。
- 强占式中断,垃圾收集时,系统将中断所有用户线程,如果发现有用户线程中断的地方不在安全点上,就恢复该线程继续执行,一会儿再重新中断,直到跑到安全点上。
- 主动式中断,不直接对线程进行中断操作,通过设置一个标志位,让各个线程不停主动轮询该标志位,当标志位为真时,线程会找到就近安全点主动中断挂起。
3.3 安全区域
为了解决线程无法响应虚拟机的中断请求,不能再走到安全的地方中断挂起自己。
安全区域是指在某一段代码片段中,引用关系不会发生变化,在该区域内进行垃圾回收都是安全的,它看起来像被拉升的安全点。
当用户线程执行到安全区域时,会标志自己已进入安全区域,当线程离开该区域时,要检查虚拟机是否已完成根节点枚举,如果完成就继续往下执行,否则就继续等待,知道收到可以离开安全区域为止。
3.4 记忆集与卡表
记忆集是用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
记录粒度划分:
- 字长精度:每个精度精确到一个机器字长,该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一个内存区域,该区域内有对象含有跨代指针。
第三种“卡精度”是用一种称为“卡表”的方式实现的记忆集,是目前最常用的记忆集实现方式,它的形式是一个字节数组,每个元素标识的一块特定大小的内存块被称之为“卡页”。一个卡页内包含多个对象,卡页内有一个或多个对象的字段存在着跨代指针,就会在该卡页元素上标识为1,认为这个元素变脏了。在垃圾收集时,筛选出卡表中变脏的元素,把它们加入GC Roots中一并扫描。
3.5 写屏障
有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用字段类型赋值的那一刻。
HotsSpot虚拟机通过写屏障技术维护卡表状态,写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,供程序执行额外的动作,在赋值前的部分的写屏障叫写前屏障,在赋值后的则叫做写后屏障。
应用写屏障后,虚拟机会为所有的赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,每次只要对引用进行更新,就会产生额外的开销。
卡表在高并发场景下存在着“伪共享”问题。当多个线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能低下。
是否开启卡表更新的条件判断参数:
-XX:+UseCondCardMark
开启会增加一次额外开销,但可以避免伪共享问题。
3.6 并发的可达性分析
从GC Roots再继续往下遍历对象图,停顿时间与JAVA堆容量直接成正比例关系。
如果用户线程和收集器并发工作,可能产生的问题:
- 把原本消亡的对象错误标记为存活。
- 把原本存活的对象错误标记为已消亡。
同时满足以下两个条件,会产生“对象消失”的问题原因:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
解决“对象消失问题”的方案:
- 增量更新,破坏第1个条件,把黑色对象插入新的白色对象引用关系时进行记录,在并发扫描结束后,再将这些记录重新扫描一次。
- 原始快照,破坏第2个条件,当灰色对象要删除对白色对象的引用关系时,就将这个要删除的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系的灰色对象为根,重新扫描一次。
网友评论