对象引用
引用计数算法
通过在对象中添加一个引用计数器,每当被引用时数值+1,当引用失效时,数值-1。当对象的引用计数器数值为0时,则说该对象没有被引用,可被垃圾回收。该算法无法解决对象之间互相循环引用的问题。
可达性分析算法
通过一系列的“GC Roots”的根对象作为起始节点集,从这些节点根据引用关系向下搜索,搜索过程所走过的路径成为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则表示该对象不可达。可被垃圾回收。
可作为GC Roots的对象包括以下几种:
1.虚拟机栈中引用的对象
2.静态属性或常量引用的对象
3.Java虚拟机内部的引用,如基本数据类型对应的class对象
4.所有被同步锁持有的对象。等等..
引用类型
强引用:强引用是最传统的引用的定义,是指在程序代码中普遍存在的引用赋值。只要强引用还存在,垃圾收集器永远无法回收被引用的对象。
软引用:软引用是用来描述一些还有用,但非必须的对象。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
虚引用:“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
垃圾收集算法
标记-清除算法
首先标记所有需要回收的对象,在标记完成后,统一清除所有标记的对象。
缺陷:一是执行效率不太稳定,如果存在大量的对象需要清除,效率会随着对象数量的增加而降低。二是会产生大量不连续的内存碎片。
标记-复制算法
它将内存容量按大小相等分为块,都只使用一块,当需要垃圾回收时,将存活的对象复制到空闲的内存块上,集中清除已经使用的这块内存中所有对象。优点是没有内存碎片,缺陷是可使用的内存空间只有原来的一半。通常用于回收年轻代的对象。
标记-整理算法
对待回收的对象进行标记,标记完成后不需要回收的对象向内存空间的一端移动,然后清理掉边界以外的内存。常用于老年代的垃圾回收。
HotSpot的算法细节实现
根结点枚举
根节点枚举必须保证在一个能保障一致性的快照中进行,不能出现在根节点枚举过程中,引用关系还在不断变化,若不能保证这一点,分析结果也不能保证准确性。这就导致垃圾回收过程必须停顿所有用户的线程。
目前主流的Java虚拟机都采用的是准确式垃圾收集,在用户停顿线程后,并不需要一个不漏的检查所有执行上下文和全局的引用位置。虚拟机有办法知道哪些地方存放着对象的引用。
安全点
安全点的是一个记录特定位置的信息,用户线程的停顿并非在任意时刻都能暂停,需要执行到特定的安全点位置后才能够暂停线程。安全点的设计解决了如何停顿用户的线程问题,让虚拟机进入垃圾回收的状态。
安全区域
安全区域是确保在一段代码片段之内,引用关系不会发生变化。因此,在这个安全区域之内的任意地方都能进行垃圾回收。安全区域可以看作是被拓展拉伸了的安全点。
记忆卡与卡表
为了解决GC Roots对象跨代引用的问题,垃圾收集器在新生代建立了名为记忆集的数据结构。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。
使用记忆集可以缩减GC Roots扫描范围的问题。但是还没解决卡表元素是如何维护的问题,例如它们是如何变脏的、谁来把它们变脏等。
写屏障
在HotSpot虚拟机里是通过写屏障技术去维护卡表状态的。写屏障可以看作是虚拟机层面堆“引用类型字段复制”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知,供程序提供额外的动作,也是说在赋值前后都在写屏障的覆盖范围内。
并发的可达性分析
三色标记算法,用于垃圾回收器升级,将STW变为并发标记。STW就是在标记垃圾的时候,必须暂停程序,而使用并发标记,就是程序一边运行,一边标记垃圾。
并发标记一共会有两个问题:一个是错标,标记过不是垃圾的,变成了垃圾(也叫浮动垃圾);第二个是本来已经当做垃圾了,但是又有新的引用指向它。
- 白色:没有检查(或者检查过了,确实没有引用指向它了)
- 灰色:自身被检查了,成员没被检查完(可以认为访问到了,但是正在被检查,就是图的遍历里那些在队列中的节点)
- 黑色:自身和成员都被检查完了
三色标记过程
第一种问题: 错标
标记过不是垃圾的,变成了垃圾(也叫浮动垃圾),如下图:标记完了E是D的一个引用,也就是说此时E是灰色的,但是D断开的对E的引用。这个浮动垃圾的问题影响不是很大,可能就是暂时的浪费一点内存,它肯定抗不过下一轮GC。
第二种问题:漏标,或者叫错杀
这个问题是比较致命的,如果错杀了,就会出现运行结果不符合预期的情况。这个是绝对不能发生的。这发生的情况只有一个,就是D是黑色的,E是灰色的,但是D又指向了G,和E断开了指向G。 因为D已经标记了是黑色,但是E断开了引用,所以G就当做了是白色的。这个时候如果不操作的话,就会把G错杀掉。这种问题是必须解决掉的
问题:
- 赋值器插入了一条从黑色对象到白色对象的引用。
- 赋值器删除了全部从灰色对象到该白色对象的直接引用或间接引用。
解决方法:
1.增量更新
增量更新破坏黑色对象重新引用白色对象的问题,当发生黑色重新插入新的指向白色的引用关系时,就将这个新插入的引用记录下来(写屏障),等并发扫描结束后,在将这些记录下来的引用重新以黑色对象为根,重新扫描一次。
2.原始快照
原始快照破坏的是删除灰色对象到白色对象引用的问题,发生从灰色对象到该白色对象的直接引用或间接引用时,就将这个要删除的引用记录下来,等并发扫描结束后,再将这个灰色对象为根,重新扫描一次。
- CMS:写屏障 + 增量更新
- G1:写屏障 + SATB
- ZGC:读屏障
经典的垃圾收集器
Serial收集器
这是一个单线程工作的收集器,并且在进行垃圾回收时,会暂停其他所有的工作线程,直到它收集结束。常用于客户端模式的默认垃圾收集器,相对于资源受限的环境,由于没有线程交互的开销,所以在垃圾收集时简单而高效。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了使用多线程外,其余的行为与Serial相同。
Parallel Scavenge收集器
Parallel Scavenge收集器是一款新生代收集器,它采用的是标记-复制算法实现的,同时也是一个多线程并行的收集器。但它专注于大吞吐量的收集器,常被称为“吞吐量优先收集器”。
吞吐量: 运行用户代码时间/运行用户代码时间+运行垃圾收集时间(即处理器总消耗时间)
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是单线程的,采用标记-整理算法实现。在客户端模式下HotSpot虚拟机采用该收集器,服务端模式下,在JDK1.5以及之前的版本搭配Parallel Scavenge收集器只用,另一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。通常搭配Parallel Scavenge收集器使用,专注于“吞吐量优先”。
CMS收集器
CMS(Concurent Mark Sweep)收集器是一种以最短停顿时间为目标的收集器,采用标记-清除算法实现。它包含以下四个过程:
- 初始标记:标记GC Roots能直接关联的对象,stop the world,但速度很快。
- 并发标记:从GC Roots直接关联的对象开始遍历整个对象图的过程,该过程很耗时,但是可与用户线程并发执行。
- 重新标记:标记由于用于线程对运行导致引用对象发生引用变更的部分(采用增量更新),stop the world,耗时相对初始标记较长。
- 并发清除:并发清理掉标记阶段已经判定为死亡的对象。
Garbage First收集器
G1基于region的堆内存布局,把连续的Java堆划分为多个大小相等的一系列独立区域(Region),每一个区域都可以根据需要扮演新生代的空间或者老年代的空间,收集器能够对扮演不同角色的region采用不同的策略去处理。将region作为单次回收的最小单位,每次收集是region的整数倍,可以避免对整个Java堆进行全区域的垃圾收集,这样就能够建立可预测的停顿时间模型。它会优先处理回收价值收益最大的那些region,这也就是Garbage First名字的由来。
步骤:
- 初始标记:仅仅只是标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的region中分配新对象。
- 并发标记:从GC Roots开始对堆中的对象进行可达性分析,递归扫描整个堆的对象图,找出要回收的对象,与用户线程并发执行。当对象扫描完成后,还要重新处理SATB(原始快照搜索)记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收:负责更新region的统计数据,对各个region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划。将region中存活的对象复制到空的region中,在清理掉整个旧的region空间。这里涉及到存活对象的移动,必须暂停用户线程。
实战:内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的控价进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,如很长的字符串、元素数量庞大的数组等。大对象直接存到老年代,这样能避免在Eden区以及两个survivor区之间来回复制,产生大量的内存复制操作。
长期存活的对象将进入老年代
虚拟机给每个对西那个定义了一个年龄计数器,存储在对象头中。Eden区的对象经过一次minor GC后并在survivor中存活,每经过一次minor GC 年龄+1,当它的年龄达到一次程度(默认15)后,就会被晋升到老年代中。
动态对象年龄断定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远都要求对西那个的年龄必须达到设置的值才晋升老年代,如果在survivor空间中低于或等于某年龄的所有对象的大小的总和大于survivor空间的一半,年龄大于或等于该年龄对象就可以直接进入老年代。
空间分配担保
在发生minor GC之前,虚拟机必须先检查老年代的最大可用的连续内存空间是否大于新生代所有对象的总空间,如果大于,说明该次minor GC是安全的。如果小于或等于,则要检查参数配置-XX:HandlePromotionFailure是否允许担保失败。如果允许,则计算本次minor GC后晋升到survivor的对象的总空间是否大于历次晋升survivor的对象的平均值,大于,则允许进行minor GC,小于获取设置为不允许担保失败,将进行一次full GC。
\