
垃圾回收机制是 JVM 自动管理内存的一种能力,主要负责清除不再使用的对象,从而避免内存泄漏或溢出。其目标就是:
垃圾回收主要是在 堆、方法区 中执行,其它的内存区域不需要回收,如下表所示:
内存区域 | 是否 GC 管理 | 是否参与回收 |
|---|---|---|
堆 | ✅ 是 | ✅ 主要回收对象分配区域 |
方法区(元空间) | ✅ 是 | ✅ 常量池、类型信息、静态变量 |
虚拟机栈/本地方法栈/程序计数器 | ❌ 否 | ❌ 线程执行过程中自动回收 |
每个对象都有一个引用计数器,每当有一个地方引用它,计数器就加一;每当引用失效时,则计数器就减一;只要计数器为 0,则说明该对象不再被使用,此时就回收该对象!
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如 Swift 就采用该算法进行内存管理。
缺点如下所示:
class Test {
Test t;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
// 下面将a、b变成null之后,由于内部的t还存在各自的引用,等待对方释放,但是又不释放自己的引用
// 就造成了循环引用问题,导致内存泄漏
a = null;
b = null;可达性分析法是 Java 采用的判断对象是否需要回收的算法!
原理:从一组叫作 GC Roots 的节点出发,向下搜索对象引用链。如果对象不能通过任何路径被 GC Roots 到达,就认为它 "不可达",则可被回收。
这种方法不会出现循环引用的问题,但相比引用计数算法来说,时间开销会大一些,因为需要遍历这颗树来判断可达性!

GC Roots 包括:
static 引用的对象通过上面确认了哪些对象应该被回收之后,就要进行垃圾对象的回收了!
下面是常见的垃圾回收算法:
算法 | 简介 | 适用场景 |
|---|---|---|
复制算法 | 把活的对象从一块复制到另一块 | 新生代,效率高 |
标记-清除 | 标记存活对象,清除未标记对象 | 老年代 |
标记-整理 | 清除后移动存活对象,解决碎片 | 老年代 |
分代收集 | 结合上面其它算法,按对象生命周期长短分类处理 | JVM 默认策略 |
标记-清除算法是最基础的回收算法,后续回收算法都是基于该算法进行改进的。
原理:首先标记所有需要回收的对象,然后在统一标记完成后回收所有被标记的对象。
缺点:① 会造成大量内存碎片,降低内存利用率。
② 标记和清除的过程效率不高。

"复制算法" 的出现是为了解决 "标记-清除算法" 的内存碎片问题。
原理:将内存空间分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域中还存活的对象复制到另一块内存中,然后再把当前内存块进行清除。
优点:避免了内存碎片问题。
缺点:① 浪费了大量内存空间。
② 如果存活对象非常多的话,复制开销会很大。

现在的商用虚拟机(包括
HotSpot)都是采用 复制算法 来 回收新生代! 新生代中98%的对象都是 "朝生夕死" 的,所以并不需要按照1:1的比例来划分内存空间,而是将新生代内存分为一块较大的Eden空间(伊甸园)和两块较小的Survivor空间(幸存者),每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。 当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。 当Survivor空间不够用时,需要依赖老年代内存进行分配担保。HotSpot默认Eden与Survivor的大小比例是8:1,也就是说Eden:From:To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。HotSpot实现的复制算法流程如下:
Eden 区满的时候,会触发第一次 MinorGC,把还活着的对象拷贝到 Survivor From 区;Eden 区再次触发 MinorGC 的时候,会扫描 Eden 区和 From 区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域,并将 Eden 和 From 区域清空。Eden 又发生 MinorGC 时,会对 Eden 和 To 区域进行垃圾回收,存活的对象复制到 From 区域,并将 Eden 和 To 区域清空。From 和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活,就存入到老年代。
由于 "复制算法" 的拷贝操作在对象存活率比较高的时候开销很大,效率会很低,所以老年代不能采用 "复制算法",而是针对 老年代 的特点,采用 "标记-整理算法"。
原理:标记过程和 "标记-清除算法" 是一样的,不同在于 "标记-整理算法" 不是直接对可回收对象进行清除,而是采用类似压缩数组元素的方式,把所有存活对象都往一侧移动,然后直接清除掉端边界以外的内存。
优点:① 避免了内存碎片问题
② 避免了 "复制算法" 中了内存浪费问题
缺点:搬运对象的开销大

分代算法实际上只是做了一个分区,把内存空间划分为新生代、老年代,然后针对两者采用不同的回收算法处理,目前大部分的 JVM 垃圾回收都采用了这种思想!
在 新生代 中每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用 "复制算法"。
而 老年代 中对象存活率高、没有额外空间对它进行分配担保,就必须采用 "标记-清理" 或者 "标记-整理" 算法。
Minor GC(新生代 GC)Minor GC 发生得非常频繁。新生代通常采用 复制算法 进行回收,只需要处理少量存活对象,因此 回收速度较快,停顿时间相对较短。Full GC(全堆 GC)Full GC 往往由老年代空间不足、Metaspace 不足或并发回收失败等原因触发。Full GC 通常会伴随 Stop-The-World,其执行时间显著长于 Minor GC,在生产环境中应尽量避免频繁发生。Full GC 并没有固定算法,具体取决于垃圾回收器实现。Serial Old 和 Parallel Old 中,Full GC 通常采用 标记-整理 算法,以避免老年代内存碎片。CMS 在正常回收阶段使用 标记-清除,但并发失败后会退化为基于 标记-整理 的 Full GC。G1 的 Full GC 是兜底机制,同样采用 标记-整理,但性能较差,因此生产环境应尽量避免。
原理大概如下所示:
1/3,老年代默认占堆内存的 2/3Eden 区、Survivor From 区、Survivor To 区默认比例是 8:1:1Eden 区,当 Eden 区内存满后将 Eden 区和 Survivor From 区存活的对象复制到 Survivor To区;Eden 区与 Survivor From 区;Survivor From 与 Survivor To 分区进行交换;Minor GC 存活对象年龄加 1,当年龄达到 15(默认值)岁时,被移到老年代;Eden 的空间无法容纳新创建的对象时,这些对象直接被移至老年代;Major GC;如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:

上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。
自从有了 Java,就有了垃圾收集器,这么多垃圾收集器其实是历史发展的产物。最早的垃圾收集器为 Serial,也就是串行执行的垃圾收集器,SerialOld 为串行的老年代收集器,而随着时间的发展,为了提升更高的性能,于是有了 Serial 多线程版的垃圾收集器 ParNew。后来人们想要更高吞吐量的垃圾收集器,吞吐量是指单位时间内成功回收垃圾的数量,于是就有了吞吐量优先的垃圾收集器 ParallelScavenge(吞吐量优先的新生代垃圾收集器)和 ParallelOld(吞吐量优先的老年代垃圾收集器)。随着技术的发展后来又有了 CMS(ConcurrentMarkSweep)垃圾收集器,CMS 可以兼顾吞吐量和以获取最短回收停顿时间为目标的收集器,在 JDK1.8 之前 BS 系统的主流垃圾收集器,而在 JDK1.8 之后,出现了第一个既不完全属于新生代也不完全属于老年代的垃圾收集器 G1(GarbageFirst),G1 提供了基本不需要停止程序就可以收集垃圾的技术。
Serial 收集器是最基本、发展历史最悠久的收集器,在 JDK1.3.1 之前是虚拟机新生代收集的唯一选择。
这个收集器是一个单线程的收集器,但它的 "单线程" 的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。
优势:采用 复制算法,简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。实际上到现在为止:它依然是虚拟机运行在 Client 模式下的默认新生代收集器。

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器。作为 Server 的首选收集器之中有一个与性能无关的很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
在
JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。 不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
与 Serial 收集器对比,ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保证可以超越 Serial 收集器。然而,随着可以使用的 CPU 的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。

Parallel Scavenge 收集器是一个新生代收集器,它也是使用 复制算法 的收集器,也是并行的多线程收集器。它使用两个参数来控制吞吐量:
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小直观上,只要最大的垃圾收集停顿时间越小,吞吐量就越高,但是 GC 停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来 10s 收集一次,每次停顿 100ms,现在变成 5s 收集一次,每次停顿 70ms。停顿时间下降的同时,吞吐量也下降了。
应用场景:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
它和其它收集器的对比如下所示:
Parallel Scavenge 与 CMS 等收集器:ParallelScavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 ParallelScavenge 收集器的目标则是达到一个可控制的吞吐量。由于与吞吐量关系密切,ParallelScavenge 收集器也经常称为 "吞吐量优先" 收集器。Parallel Scavenge 与 ParNew 收集器:ParallelScavenge 收集器与 ParNew 收集器的一个重要区别是它具有自适应调节策略。
GC自适应的调节策略:Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 "标记-整理" 算法。应用场景如下所示:
Client 模式:Serial Old 收集器的主要意义也是在于给 Client 模式下的虚拟机使用。Server 模式:如果是 Server 模式,那么它主要还有两大用途:一种用途是在 JDK1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程、"标记-整理" 算法。
应用场景:在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 配合 Parallel Old收集器。
这个收集器是在 JDK1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是,如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old 收集器外别无选择(Parallel Scavenge 收集器无法与 CMS 收集器配合工作)。由于老年代 SerialOld 收集器在服务端应用性能上表现不佳,使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合给力。直到 Parallel Old 收集器出现后,"吞吐量优先" 收集器终于有了比较名副其实的应用组合。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,而 CMS 收集器就非常符合这类应用的需求!
CMS 收集器是基于 "标记-清除" 算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为四个步骤:
GCRoots 能直接关联到的对象,速度很快,需要 Stop The World。GCRoots Tracing 的过程。Stop The World。由于整个过程中耗时最长的 并发标记 和 并发清除 过程收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

优点:并发回收、低停顿
缺点:
CMS 收集器对 CPU 资源非常敏感CPU 资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是 (CPU数量+3)/4,也就是当 CPU 在四个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的增加而下降。但是当 CPU 不足四个时,CMS 对用户程序的影响就可能变得很大。CMS 收集器无法处理浮动垃圾CMS 收集器无法处理浮动垃圾,可能出现 ConcurrentModeFailure 失败而导致另一次 FullGC 的产生。CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为 "浮动垃圾"。CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。CMS 运行期间预留的内存无法满足程序需要,就会出现一次 ConcurrentModeFailure 失败,这时虚拟机将启动后备预案:临时启用 SerialOld 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。CMS 收集器会产生大量空间碎片CMS 是一款基于 "标记-清除" 算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 FullGC。G1(Garbage First)垃圾回收器是用在堆内存很大的情况下,将堆划分为多个 Region,通过收益优先回收垃圾最多的区域。它支持并发标记和混合回收,显著减少 Full GC 的触发和停顿时间。
新生代使用 复制算法,老年代采用 标记-整理。Mixed GC 回收 young + 部分 old,实现可预测停顿时间,是服务器端长时间运行应用的默认 GC。
设计理念:
Full GC 的问题G1 垃圾回收器回收 region 的时候基本不会 STW,而是基于 most garbage 优先回收的策略来对 region 进行垃圾回收的。(整体来看是基于 "标记-整理" 算法,从局部来看也就是两个 region 之间则是基于 "复制" 算法)
无论如何,G1 收集器采用的算法都意味着一个 region 有可能属于 Eden、Survivor 或者 Tenured 内存区域。
图中的 E 表示该 region 属于 Eden 内存区域,S 表示属于 Survivor 内存区域,T 表示属于 Tenured 内存区域,空白的表示未使用的内存空间。
G1 垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,如图中的 H 块,这种内存区域主要用于存储大对象,即大小超过一个 region 大小 50% 的对象。

Minor GCMixed GCMinor GC 少,但比 Full GC 多Full GC(兜底)Mixed GC 无法腾出足够空间Humongous Objects
在 G1 垃圾收集器中,年轻代的垃圾回收过程使用 "复制" 算法。把 Eden 区和 Survivor 区的对象复制到新的 Survivor 区域。

对于老年代上的垃圾收集,G1 垃圾收集器也分为四个阶段:
GC Roots 可直接访问的对象,主要是 young Region 的根对象和老年代直接引用的对象。CMS 类似,但 G1 将其与 Minor GC 相结合,在触发 Mixed GC 或并发标记前同时完成初始标记,无需单独 STW。Region 的存活率,为 Mixed GC 或 Cleanup 阶段选择垃圾最多的 Region 做准备。Region,以最小化停顿时间。SATB(Snapshot-At-The-Beginning)技术提高效率,保证新增可达对象不会被误回收。Region,释放空间,准备下一轮 Mixed GC。STW 执行,但只针对被选中的 Region,而不是整个老年代。
G1 是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器。如果追求低停顿,G1 可以作为选择;如果追求吞吐量,G1 并不带来特别明显的好处。
特性 | G1 | CMS |
|---|---|---|
老年代回收 | Mixed GC | 并发标记-清除 |
Full GC | 避免 | 容易触发 |
STW 时间 | 可控 | 不可控 |
内存碎片 | 标记-整理 | 可能有碎片 → 需要 Full GC |
适用场景 | 长时间运行,响应要求可控 | 老、旧服务端应用 |
我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的 From 区(S0区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 From 区,有时候在 Survivor 的 To 区(S1区),居无定所。
直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每次 GC 加一岁)然后被回收了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。