垃圾收集 (Garbage Collection,GC)
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
判断一个对象是否可被回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
- 优点:执行效率高,程序执行受影响较小。
- 缺点:无法检测出循环引用的情况,引起内存泄漏。
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用可达性分析算法来判断对象是否可被回收,GC Roots 一般包含以下几种:
- 虚拟机栈中局部变量表中引用的对象(栈帧中的本地方法变量表)
- 本地方法栈中 JNI(Native方法) 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
- 活跃线程的引用对象
方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
finalize()
类似 C++ 的析构函数(注意:只是类似,C++ 析构函数调用确定,而 finalize() 方法是不确定的),用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当垃圾回收器要宣告一个对象死亡时,至少要经历两次标记过程。
如果对象在进行可达性分析以后,没有与GC Root 直接相连接的引用量,就会被第一次标记,并且判断是否执行 finalize() 方法;
如果这个对象覆盖了 finalize() 方法,并且未被引用,就会被放置于 F-Queue 队列,稍后由虚拟机创建的一个低优先级的 finalize() 线程去执行触发 finalize() 方法,在该方法中让对象重新被引用,从而实现自救。但是该线程的优先级比较低,执行过程随时可能会被终止。此外,自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
JDK1.2 之前,Java 中引用定义:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。
强引用 (Strong Reference)
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
当内存空间不足,JVM 抛出 OOM Error 终止程序也不会回收具有强引用的对象,只有通过将对象设置为 null 来弱化引用,才能使其被回收。
软引用 (Soft Reference)
表示对象处在有用但非必须的状态。
被软引用关联的对象只有在内存不够的情况下才会被回收。可以用来实现内存敏感的高速缓存。
软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
弱引用 (Weak Reference)
表示非必须的对象,比软引用更弱一些。适用于偶尔被使用且不影响垃圾收集的对象。
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,JVM 就会把这个弱引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。
使用 WeakReference 类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用 (Phantom Reference)
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
不会决定对象的生命周期,任何时候都可能被垃圾回收器回收。必须和引用队列 ReferenceQueue 联合使用。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知,起哨兵作用。具体来说,就是通过判断引用队列 ReferenceQueue 是否加入虚引用来判断被引用对象是否被 GC(垃圾回收线程) 回收:当 GC 准备回收一个对象时,如果发现它还仅有虚引用指向它,就会在回收该对象之前,把这个虚引用加入到与之关联的引用队列 ReferenceQueue 中。如果一个虚引用对象本身就在引用队列中,就说明该引用对象所指向的对象被回收了。
使用 PhantomReference 来创建虚引用。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
// 虚引用必须和引用队列 ReferenceQueue 联合使用
PhantomReference<Object> pf = new PhantomReference<Object>(obj, queue);
obj = null;
总结:
引用类型 | 被垃圾回收的时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足的时候 | 对象缓存 | 内存不足时终止 |
弱引用 | 在垃圾回收的时候 | 对象缓存 | GC运行后终止 |
虚引用 | Unknown | 标记、哨兵 | Unknown |
垃圾收集算法
“标记-清除” 算法
在标记阶段,从根集合进行扫描,会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
”标记-整理“ 算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片
不足:需要移动大量对象,处理效率比较低。
”复制“ 算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法:
- 新生代:新生代对象存活时间很短,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
- 老年代:老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
Stop-the-World & SafePoint
Stop-the-World
所谓 Stop-the-World(简称 STW),指的是 JVM 由于要执行 GC 而停止了应用程序的执行 :
- 可达性分析算法中 GC Roots 会导致所有 Java 执行线程停顿,原因如下:
- 分析工作必须在一一个能确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
- 被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 影响用户体验,所以需要减少 STW 的发生。
STW 事件和采用哪款垃圾收集器无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 STW 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用 System.gc() 这样会导致 STW 的发生。
目前,降低系统的停顿时间两种算法:增量收集算法和分区算法。
增量收集算法
基本思想:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。垃圾收集线程一次只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
不足:由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
基本思想:一般来说,在相同条件下,堆空间越大,一次 GC 时所需要的时间就长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。
注意分区算法与分代收集算法是不同的:分代收集算法将按照对象的生命周期长短划分成两个部分,而分区算法将整个堆空间划分成连续的不同小区间,其中每个小区间都独立使用,独立回收,这样可以控制一次回收多少个小区间。
总结:
- 增量收集算法是将总的收集量一部分一部分的去执行
- 分区算法是将总的内存空间分为小分区,一次可控的去收集多少个小区间。
SafePoint
程序执行时并非可以在任何地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始 GC,这些位置称为Safepoint 。
SafePoint 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太频繁可能导致运行时的性能问题。
大部分指令的执行时间都非常短暂,通常会根据是否具有让程序长时间执行的特征为标准。比如选择一些执行时间较长的指令作为 SafePoint,如方法调用、循环跳转和异常跳转等。
垃圾收集器
评估 GC 的性能指标
吞吐量
运行用户代码的时间占总运行时间的比例。其中总运行时间 = 程序的运行时间 + 内存回收的时间。
比如:虚拟机总共运行了100分钟, 其中垃圾收集花掉1分钟,那吞吐量就是99%。
暂停时间
执行垃圾收集时,程序的工作线程被暂停的时间。
”高吞吐量” 和 ”低暂停时间” 是矛盾的:
- 如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致 GC 需要更长的暂停时间来执行内存回收。
- 如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。
现在标准:在最大吞吐量优先的情况下,降低停顿时间。
经典垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 JDK 9 取消了对 Serial+CMS、
ParNew+Serial Old 这两个组合的支持。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
1. Serial 收集器
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
2. ParNew 收集器
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
3. Parallel Scavenge 收集器
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4. Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5. Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6. CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下六个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:清理垃圾对象,不需要停顿。
- 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
CMS 具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
7. G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,保证了 G1 在有限的时间内可以获取尽可能高的收集效率。这种方式的侧重点在于回收垃圾最大量的 Region),G1 (Garbage First) 因此得名。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
与其他垃圾收集器相比,G1 具备如下特点:
-
并行和并发
并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力。此时用户线程STW
并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
-
分代收集
将堆空间分为若干个区域(Region) ,这些区域中包含了逻辑上的年轻代和老年代。 -
空间整合
G1将内存划分为一个个的 Region,内存的回收以 Region 作为基本单位。从局部(两个 Region 之间)上来看是基于“复制”算法,整体来看是基于“标记 - 整理”算法,这两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。 尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
-
可预测的停顿
G1 除了追求低停顿外(G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制),还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
相较于CMS,G1还不具备全方位、压倒性优势:
比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载(overload) 都要比 CMS 要高。从使用经验上来说,对于小内存应用,CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在6- 8GB之间。
7 种经典垃圾收集器总结如下:
垃圾收集器 | 运行状态 | 作用位置 | 收集算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | “复制”算法 | 响应速度优先 | 单 CPU 下的 Client 模式 |
ParNew | 并行 | 新生代 | “复制”算法 | 响应速度优先 | 多 CPU 的 Server 模式配合 CMS 使用 |
Parallel | 并行 | 新生代 | “复制”算法 | 吞吐量优先 | 后台运算且不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | “标记-整理”算法 | 响应速度优先 | 单 CPU 下的 Client 模式 |
Parallel Old | 并行 | 老年代 | “标记-整理”算法 | 吞吐量优先 | 后台运算且不需要太多交互的场景 |
CMS | 并发 | 老年代 | “标记-清除”算法 | 响应速度优先 | 互联网或 B/S 业务 |
G1 | 并发 + 并行 | 新生代+ 老年代 | “标记-整理” + “复制” 算法 | 响应速度优先 | 面向服务端应用 |
参考:
标签:收集,收集器,对象,回收,算法,垃圾,JVM,引用 From: https://www.cnblogs.com/i9code/p/17986071