这是「死磕P7」系列第 004 篇文章,欢迎大家来跟我一起 死磕 100 天,争取在 2025 年来临之际,给自己一个交代。
上一篇给大家介绍了 JVM 为了垃圾回收的方便将 堆 空间划分为了 年轻代 和 老年代,并且给年轻代又划分为 Eden,S0, S1 三个区域,那到底什么是垃圾,如何确认对象是否还需要继续留存呢?
本节课就来学习一下垃圾回收的考点之一:什么是垃圾?
在 JVM 语境下,“垃圾”指的是死亡的对象所占据的空间
所谓“垃圾收集”,就是将已分配出去、但不再使用的内存回收回来,以便能再次分配
如何判断一个对象是否死亡(即不可能再被任何途径使用)?通常有以下两种方法:
为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。当有地方引用它时,计数器加一;引用失效时减一。当某个对象的引用计数为零时,说明该对象已死亡,便可以被回收了。
通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为”引用链”(Reference Chain),若某个对象到 GC Roots 间没有任何引用链相连(或者用图论的话来说就是从 GC Roots 到这个对象不可达时)则证明此对象是不可能再被使用的。
GC Roots 可理解为「堆外指向堆内的引用」。在 Java 技术体系中,固定可作为 GC Roots 的对象包括以下几种(看 2 遍就好了):
上面提到了引用,那什么是引用呢,Java 中都有哪些引用呢?
无论是引用计数法还是可达性分析法,二者都离不开「引用」,Java 中定义了 4 种引用类型
简单记一下就好了~
例如:Object obj = new Object()
特点:无论任何情况,只要强引用存在,垃圾收集器永不回收被引用的对象
场景:用于一些还有用、但非必须的对象
时机:被软引用关联的对象,在系统将发生 OOM 前,回收这些内存
实现:java.lang.ref.SoftReference
场景:非必须对象,比软引用更弱
时机:被弱引用关联的对象只能生存到下一次垃圾收集发生(无论内存是否充足都会回收)
实现:java.lang.ref.WeakReference
又称“幽灵引用”或“幻影引用”
特点:最弱的引用,是否存在完全不会影响其生存时间,无法通过它获取对象实例
唯一目的:该对象被回收时收到一个系统通知
实现:java.lang.ref.PhantomReference
提到算法,是不是很神秘?没事,有些算法还是比较容易的,比如:把大象放进冰箱需要几步?
垃圾回收算法主要有 4 种:标记复制、标记清除、标记整理、分代收集
把内存空间划为两个相等的区域,每次只使用其中一个区域。
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩了一半,倘若存活对象多,效率会大大降低
一般不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。
当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (即“浪费”了 10% 的新生代空间)。
由于无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖老年代进行分配担保,也就是直接进入老年代(相当于“兜底方案”)。
现在大多数虚拟机对新生代的垃圾回收都采用的这种算法,并且其他算法也是在这个算法的基础上进行改进优化。
标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
优点:实现简单
缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。
标记-整理(Mark-Compact)算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-清除算法与标记-整理算法区别:前者是一种非移动式的回收算法,后者是移动式的。
首先标记出哪些对象可被回收,在标记完成后,将对象向一端移动,然后直接清理掉边界以外的内存。
优点:解决了产生大量不连续碎片问题
根据各个年代的特点选择合适的垃圾收集算法。
新生代中因为对象都是”朝生夕死的”,90% 以上的对象存活率很低,适用于复制算法,复制算法比较适合用于存活率低的内存区域。
它效率高且无碎片问题,无非就是占点内存而已,空间换时间。
每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。
老年代的垃圾回收(又称Major GC)通常使用 「标记-清理」或「标记-整理」算法(跟 JDK 版本还有指定配置有关)。
回收算法只是概念,理论层面的,具体实现上就对应着垃圾收集器了,下面介绍一下垃圾收集器。
衡量垃圾收集器优劣的指标主要有三个:
此三者构成了一个「三元悖论」难以同时满足。
收集算法只是内存回收的方法论,而垃圾收集器才是内存回收的具体实现(可理解为“接口”与“实现类”的关系)。
下面这个图,刚开始可能看不明白,MinorGC 对应年轻代可用的垃圾收集器,MajorGC 对应老年代可用的垃圾收集器,中间的 -XX 参数表示指定使用哪两组收集器来完成 MinorGC, MajorGC, 下图中总共有 5 组可用的组合(当然还有未画的,做个了解即可)。
下面来具体看一下上面提到的垃圾收集器是什么吧!
单线程收集器,历史最悠久的垃圾收集器,工作时必须暂停所有用户线程(STW, Stop The World)。
Serial收集器采用「标记-复制」主要针对新生代的收集。
Serial Old收集器采用「标记-整理」算法主要针对老年代的收集器。
实现简单高效,但会停顿。
Serial 配合 Serial Old 示意图:
ParNew 收集器实质上是 Serial 收集器的多线程并行版本,同样作用于 MinorGC。
ParNew 配合 Serial Old 示意图:
Parallel 是新生代收集器,同样采用「标记-复制」算法,并行收集的多线程收集器,也称“吞吐量优先收集器”。
能更高效地利用 CPU 资源,尽快完成计算任务(主要适合在后台运算而不需要太多交互的任务)。
Parallel Old采用「标记-整理」算法主要针对老年代的收集器。
Parallel 配合 Parallel Old 示意图:
在注重吞吐量或者处理器资源较为稀缺的场合,都可以考虑 Parallel Scavenge + Parallel Old 收集器的组合。
CMS(Concurrent Mark Sweep)收集器是一种以「获取最短回收停顿时间」为目标的收集器。
它基于「标记-清除」算法实现,运作过程分为四步:
目前很大一部分 Java 应用集中在互联网网站或者 B/S 系统的服务上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS 收集器非常符合这类应用的需求。
CMS 收集器示意图:
G1 收集器是收集器技术发展最前沿的成果,面向服务端应用的收集器,能充分利用多CPU、多核环境。
相比与 CMS 收集器,G1 收集器两个最突出的改进是:
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
G1 收集器的定位是「CMS 收集器的替代者和继承人」,这里只做简单描述:
此外,还有一些更为先进的低延迟收集器,比如 OracleJDK 11 加入的 ZGC,RedHat 公司的 Shenandoah 收集器,知道一点即可。
本文又是一片硬骨头,放松心态,当个小说读两遍,能记多少记多少吧!
回顾一下本篇内容就是,了解了对象是否存活的 2 种策略:引用计数法和可达性分析法,了解了 Java 的 4 类引用分类:强软弱虚,知道了垃圾回收的常见算法:标记-复制,标记-清除,标记-整理,并根据图示能理解其大概行为,最后介绍了常见的垃圾收集器:Serial, ParNew, Parallel, CMS, G1, ZGC, 知道其大概优缺点即可。
本次的分享到此结束,希望对你有所帮助。
如果你对我分享的内容感兴趣,欢迎扫码关注公众号:新质程序猿,并设置星标,推送更实时哟!