垃圾回收机制 —— 整理介绍

垃圾回收机制的意义

在 C++ 开发中管理内存是一个很麻烦的问题,而 Java 引入了垃圾回收机制,开发者不须要手动去管理内存的分配和回收问题,一切都交给 JVM 经过垃圾回收机制处理,同时有效的防止了内存泄漏的问题。java

Java 语言规范中并无明确的指定 JVM 使用哪一种回收算法,但一般回收算法主要作 2 件事情:算法

  • 发现无用的对象
  • 回收被无用对象占用的内存空间

如何发现无用的对象

Reference Counting(引用计数)

早期的 JVM 利用的策略是引用计数。通常来讲,堆中的每个对象对应一个引用计数器。数组

  • 当建立一个对象并分配给一个引用变量时,对象的引用计数器置为 1。
  • 当任何其余引用变量被赋值为这个对象的引用时,引用计数器加 1。
  • 但当一个对象的某个引用变量超过了生命周期或者被设置为一个新值时,该对象的引用计数器减 1。
  • 当一个对象被回收时,它引用的任何对象的引用计数器都减 1。
  • 任何引用计数器为 0 的对象能够被看成无用的对象。

利用这种方法判断无用的对象,实现简单高效,对程序须要不被长时间打断的环境比较有利。但这种方法没法解决循环引用的问题:多线程

Object o1 = new Object();
Object o2 = new Object();

o1.object = o2;
o2.object = o1;

o1 = null;
o2 = null;

o1,o2 最后都被赋值为 null,也就是说以前 o1,o2 所引用的对象都没法被访问。可是因为两个对象互相引用对方,因此它们的引用计数器都不为 0,因此垃圾收集器没法回收它们。并发

Tracing(追踪)算法

如今垃圾回收机制都使用根搜索算法,把全部的引用关系看做一张图,根集(root set)做为图的起点,所谓根集就是正在执行的 Java 程序能够访问的引用变量的集合(包括局部变量、参数、类变量)。从根集 开始,寻找可达的对象,找到可达的对象后继续寻找这个对象的引用对象,当全部的可达的或间接可达的对象寻找完毕,剩余的则被认为是不可达的游离对象,即无用的对象。线程

image

典型的垃圾收集算法

1. Mark - Sweep(标记 - 清除)算法

这是最基础的垃圾收集算法,标记 - 清除算法是从根集进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。该方法不移动对象,仅仅回收未标记的对象,在存活对象较多的状况下效率极高。可是这个算法也容易产生内存碎片,过多的内存碎片会致使为大对象分配空间时没法找到足够的空间,而触发新一次的垃圾收集动做。code

image

2. Mark - Compact(标记 - 整理)算法

标记 - 整理算法在标记阶段与标记 - 清除算法一致,可是为了解决内存碎片的问题,在完成标记后,并不直接清理未标记的对象,而是将存活的对象都向一端移动,而后清理掉存活对象端边界之外的内存。通常在这种算法的实现中,都增长了句柄和句柄表,也形成了必定的开销。对象

image

3. Copying(复制)算法

复制算法会将堆内存分为使用区和空闲区两部分。每次只使用其中的使用区,当使用区用完,就进行一次扫描标记,将还存活的对象复制到空闲区上,而后再将使存区进行一次清理。这样,空闲区成为了使用区,原来的使用区变成了空闲区。这钟也解决了内存碎片的问题。一种典型的基于 Copying 算法的垃圾回收是 Stop - Copy 算法,它在使用区和空闲区的切换过程当中,程序暂停执行。blog

这种算法虽然简单高效,且不易产生内存碎片,却对内存空间的利用付出了高昂的代价,内存使用率只有一半。并且,存活的对象若是数量居多,那么算法效率将大大下降。生命周期

image

4. Generational(分代)算法

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期不一样将内存划分为若干个不一样的区域。通常状况下将堆区划分为新生代(Young Generation)和老年代(Tenured Generation)。不一样生命周期的对象能够采起不一样的回收算法,以提升回收效率。

image

新生代(Young Generation)

全部新生成的对象首先都是放在新生代中,在新生代的目标是尽量快的收集生命周期短暂的对象。

目前大部分垃圾收集器对于新生代都采用 Copying 算法,由于新生代中每次都要回收大部分对象,存活的对象较少,因此复制操做较少。通常来讲,新生代的内存按照 8:1:1 的比例划分为一个 Eden 区和两个较小的 Survivor0,Survivor1 区。大部分对象在 Eden 区生成,回收时先将 Eden 区中存活的对象复制到一个 Survivor0 区中,而后清空 Eden 区。当这个 Survivor0 区也存放满时,则将 Eden 区和 Survivor0 区的存活对象复制到另外一个 Survivor1 区,而后清空 Eden 和 Survivor0 区,此时 Survivor0 区是空的,而后将 Survivor0 区和 Survivor1 区交换,即保持 Survivor1 区为空,如此往复。

当 Survivor1 区不足以存放 Eden 和 Survivor0 的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次 Full GC,也就是新生代、老年代都进行回收。

新生代发生的 GC 也叫作 Minor GC,MinorGC 发生频率比较高(不必定等 Eden 区满了才触发)。

老年代(Tenured Generation)

在年轻代中经历了屡次垃圾回收后仍然存活的对象,到达必定次数后就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。因此每次回收都只回收少许对象,通常使用的是 Mark - Compact 算法。

通常来讲,大对象会被直接分配到老年代,所谓的大对象是指须要大量连续存储空间的对象,最多见的一种大对象就是大数组。

老年代内存比新生代也大不少,当老年代内存满时触发 Major GC 即 Full GC,发生的频率比较低。

持久代(Permanent Generation)

在堆区以外还有一个就是持久代(Permanent Generation),它用于存放静态文件,如 class 类、常量、方法描述等。持久代的回收主要回收两部份内容:废弃的常量和无用的类。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。

典型的垃圾收集器

垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面 JVM 提供的几种垃圾收集器,用户能够根据本身的需求组合出各个年代使用的收集器。

image

1. Serial/Serial Old

它们都是一个单线程的收集器,在进行垃圾收集时,必须暂停全部的用户线程。它的优势是实现简单高效,可是缺点是会给用户带来停顿。Serial 是针对新生代的收集器,采用的是 Copying 算法。Serial Old 是针对老年代的收集器,采用的是 Mark - Compact 算法。

2. ParNew

ParNew 收集器是 Serial 收集器的多线程版本,使用多个线程进行垃圾收集,是针对新生代的收集器,采用的是 Stop - Copy 算法。

3. Parallel Scavenge / Parallel Old

Parallel Scavenge 收集器是一个针对新生代的多线程收集器(并行收集器),它在回收期间不须要暂停其余用户线程,其采用的是 Copying 算法,该收集器与前两个收集器有所不一样,它主要是为了达到一个可控的吞吐量。Parallel Old 是 Parallel Scavenge 收集器的老年代版本(并行收集器),使用多线程和 Mark - Compact 算法。

4. CMS(Concurrent Mark Sweep)

它是一种以获取最短回收停顿时间为目标的收集器,它是一种针对老年代的并发收集器,采用的是 Mark - Sweep 算法。

5. G1

G1 收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多 CPU、多核环境。所以它是一款并行与并发收集器,而且它能创建可预测的停顿时间模型。

垃圾回收执行机制的分类

因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC 有两种类型:Scavenge GC 和 Full GC。

Scavenge GC

通常状况下,当新对象生成,而且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,而且把尚且存活的对象移动到 Survivor 区,而后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到老年代。由于大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,因此 Eden 区的 GC 会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。

Full GC

对全部年代进行整理,包括新生代、老年代和持久代。Full GC 由于须要对整个内存进行回收,因此比 Scavenge GC 要慢,所以应该尽量减小 Full GC 的次数。在对 JVM 调优的过程当中,很大一部分工做就是对于 Full GC 的调节。有以下缘由可能致使 Full GC:

  • 年老代被写满
  • 持久代被写满
  • System.gc() 被显示调用
  • 上一次 GC 以后堆中的各域分配策略动态变化
相关文章
相关标签/搜索