深入理解JVM虚拟机3垃圾回收器详解

31次阅读

共计 12533 个字符,预计需要花费 32 分钟才能阅读完成。

JVM GC 基本原理与 GC 算法

微信公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站。作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!(关注公众号后回复”Java“即可领取 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)</font>

                     

Java 的内存分配与回收全部由 JVM 垃圾回收进程自动完成。与 C 语言不同,Java 开发者不需要自己编写代码实现垃圾回收。这是 Java 深受大家欢迎的众多特性之一,能够帮助程序员更好地编写 Java 程序。

下面四篇教程是了解 Java 垃圾回收(GC)的基础:

  1. 垃圾回收简介
  2. 圾回收是如何工作的?
  3. 垃圾回收的类别

这篇教程是系列第一部分。首先会解释基本的术语,比如 JDK、JVM、JRE 和 HotSpotVM。接着会介绍 JVM 结构和 Java 堆内存结构。理解这些基础对于理解后面的垃圾回收知识很重要。

Java 关键术语

  • JavaAPI:一系列帮助开发者创建 Java 应用程序的封装好的库。
  • Java 开发工具包(JDK):一系列工具帮助开发者创建 Java 应用程序。JDK 包含工具编译、运行、打包、分发和监视 Java 应用程序。
  • Java 虚拟机(JVM):JVM 是一个抽象的计算机结构。Java 程序根据 JVM 的特性编写。JVM 针对特定于操作系统并且可以将 Java 指令翻译成底层系统的指令并执行。JVM 确保了 Java 的平台无关性。
  • Java 运行环境(JRE):JRE 包含 JVM 实现和 Java API。

Java HotSpot 虚拟机

每种 JVM 实现可能采用不同的方法实现垃圾回收机制。在收购 SUN 之前,Oracle 使用的是 JRockit JVM,收购之后使用 HotSpot JVM。目前 Oracle 拥有两种 JVM 实现并且一段时间后两个 JVM 实现会合二为一。

HotSpot JVM 是目前 Oracle SE 平台标准核心组件的一部分。在这篇垃圾回收教程中,我们将会了解基于 HotSpot 虚拟机的垃圾回收原则。

JVM 体系结构

下面图片总结了 JVM 的关键组件。在 JVM 体系结构中,与垃圾回收相关的两个主要组件是堆内存和垃圾回收器。堆内存是内存数据区,用来保存运行时的对象实例。垃圾回收器也会在这里操作。现在我们知道这些组件是如何在框架中工作的。

Java 堆内存

我们有必要了解堆内存在 JVM 内存模型的角色。在运行时,Java 的实例被存放在堆内存区域。当一个对象不再被引用时,满足条件就会从堆内存移除。在垃圾回收进程中,这些对象将会从堆内存移除并且内存空间被回收。堆内存以下三个主要区域:

  1. 新生代(Young Generation)

    • Eden 空间(Eden space,任何实例都通过 Eden 空间进入运行时内存区域)
    • S0 Survivor 空间(S0 Survivor space,存在时间长的实例将会从 Eden 空间移动到 S0 Survivor 空间)
    • S1 Survivor 空间(存在时间更长的实例将会从 S0 Survivor 空间移动到 S1 Survivor 空间)
  2. 老年代(Old Generation)实例将从 S1 提升到 Tenured(终身代)
  3. 永久代(Permanent Generation)包含类、方法等细节的元信息

永久代空间在 Java SE8 特性中已经被移除。

Java 垃圾回收是一项自动化的过程,用来管理程序所使用的运行时内存。通过这一自动化过程,JVM 解除了程序员在程序中分配和释放内存资源的开销。

启动 Java 垃圾回收

作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。System.gc()Runtime.gc() 用来请求 JVM 启动垃圾回收。

虽然这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM 负责。JVM 可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由 JVM 决定,并且取决于堆内存中 Eden 区是否可用。JVM 将这个选择留给了 Java 规范的实现,不同实现具体使用的算法不尽相同。

毋庸置疑,我们知道垃圾回收过程是不能被强制执行的。我刚刚发现了一个调用 System.gc() 有意义的场景。通过这篇文章了解一下适合调用 System.gc() 这种极端情况。

各种 GC 的触发时机(When)

GC 类型

说到 GC 类型,就更有意思了,为什么呢,因为业界没有统一的严格意义上的界限,也没有严格意义上的 GC 类型,都是左边一个教授一套名字,右边一个作者一套名字。为什么会有这个情况呢,因为 GC 类型是和收集器有关的,不同的收集器会有自己独特的一些收集类型。所以作者在这里引用 R 大 关于 GC 类型的介绍,作者觉得还是比较妥当准确的。如下:

  • Partial GC:并不收集整个 GC 堆的模式

    • Young GC(Minor GC):只收集 young gen 的 GC
    • Old GC:只收集 old gen 的 GC。只有 CMS 的 concurrent collection 是这个模式
    • Mixed GC:收集整个 young gen 以及部分 old gen 的 GC。只有 G1 有这个模式
  • Full GC(Major GC):收集整个堆,包括 young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

触发时机

上面大家也看到了,GC 类型分分类是和收集器有关的,那么当然了,对于不同的收集器,GC 触发时机也是不一样的,作者就针对默认的 serial GC 来说:

  • young GC:当 young gen 中的 eden 区分配满的时候触发。注意 young GC 中有部分存活对象会晋升到 old gen,所以 young GC 后 old gen 的占用量通常会有所升高。
  • full GC:当准备要触发一次 young GC 时,如果发现统计数据说之前 young GC 的平均晋升大小比目前 old gen 剩余的空间大,则不会触发 young GC 而是转为触发 full GC(因为 HotSpot VM 的 GC 里,除了 CMS 的 concurrent collection 之外,其它能收集 old gen 的 GC 都会同时收集整个 GC 堆,包括 young gen,所以不需要事先触发一次单独的 young GC);或者,如果有 perm gen 的话,要在 perm gen 分配空间但已经没有足够空间时,也要触发一次 full GC;或者 System.gc()、heap dump 带 GC,默认也是触发 full GC。

FULL GC 触发条件详解

除直接调用 System.gc 外,触发 Full GC 执行的情况有如下四种。

1. 旧生代空间不足

旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行 Full GC 后空间仍然不足,则抛出如下错误:

java.lang.OutOfMemoryError: Java heap space 

为避免以上两种状况引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

2. Permanet Generation 空间满

Permanet Generation 中存放的为一些 class 的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space 

为避免 Perm Gen 占满造成 Full GC 现象,可采用的方法为增大 Perm Gen 空间或转为使用 CMS GC。

3. CMS GC 时出现 promotion failed 和 concurrent mode failure

对于采用 CMS 进行旧生代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当这两种状况出现时可能会触发 Full GC。

promotion failed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

应对措施为:增大 survivor space、旧生代空间或调低触发并发 GC 的比率,但在 JDK 5.0+、6.0+ 的版本中有可能会由于 JDK 的 bug29 导致 CMS 在 remark 完毕后很久才触发 sweeping 动作。对于这种状况,可通过设置 -XX: CMSMaxAbortablePrecleanTime=5(单位为 ms)来避免。

4. 统计得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间

这是一个较为复杂的触发情况,Hotspot 为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行 Minor GC 时,做了一个判断,如果之前统计所得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发 Full GC。

例如程序第一次触发 Minor GC 后,有 6MB 的对象晋升到旧生代,那么当下一次 Minor GC 发生时,首先检查旧生代的剩余空间是否大于 6MB,如果小于 6MB,则执行 Full GC。

当新生代采用 PS GC 时,方式稍有不同,PS GC 是在 Minor GC 后也会检查,例如上面的例子中第一次 Minor GC 后,PS GC 会检查此时旧生代的剩余空间是否大于 6MB,如小于,则触发对旧生代的回收。

除了以上 4 种状况外,对于使用 RMI 来进行 RPC 或管理的 Sun JDK 应用而言,默认情况下会一小时执行一次 Full GC。可通过在启动时通过 - java -Dsun.rmi.dgc.client.gcInterval=3600000 来设置 Full GC 执行的间隔时间或通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc。

总结一下就是:

 Minor GC,Full GC 触发条件

Minor GC 触发条件:当 Eden 区满时,触发 Minor GC。

Full GC 触发条件:

(1)调用 System.gc 时,系统建议执行 Full GC,但是不必然执行

(2)老年代空间不足

(3)方法去空间不足

(4)通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存

(5)由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

什么是 Stop the world

Java 中 Stop-The-World 机制简称 STW,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java 中一种全局暂停现象,全局停顿,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互;这些现象多半是由于 gc 引起。

GC 时的 Stop the World(STW)是大家最大的敌人。但可能很多人还不清楚,除了 GC,JVM 下还会发生停顿现象。

JVM 里有一条特殊的线程--VM Threads,专门用来执行一些特殊的 VM Operation,比如分派 GC,thread dump 等,这些任务,都需要整个 Heap,以及所有线程的状态是静止的,一致的才能进行。所以 JVM 引入了安全点 (Safe Point) 的概念,想办法在需要进行 VM Operation 时,通知所有的线程进入一个静止的安全点。

除了 GC,其他触发安全点的 VM Operation 包括:

1. JIT 相关,比如 Code deoptimization, Flushing code cache;

2. Class redefinition (e.g. javaagent,AOP 代码植入的产生的 instrumentation);

3. Biased lock revocation 取消偏向锁;

4. Various debug operation (e.g. thread dump or deadlock check);

Java 垃圾回收过程

垃圾回收是一种回收无用内存空间并使其对未来实例可用的过程。

Eden 区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。

注意:如果你不能理解这些词汇,我建议你阅读这篇 垃圾回收介绍,这篇教程详细地介绍了内存模型、JVM 架构以及这些术语。

Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。

(译注:此处不应该是 Eden 和 S0 中存活的都移到 S1 么,为什么会先移到 S0 再从 S0 移到 S1?)

死亡的实例(不再被引用)被标记为垃圾回收。根据垃圾回收器(有四种常用的垃圾回收器,将在下一教程中介绍它们)选择的不同,要么被标记的实例都会不停地从内存中移除,要么回收过程会在一个单独的进程中完成。

老年代:老年代(Old or tenured generation)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC 周期时,在 S1 Survivor 区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。

老年代 GC(Major GC):相对于 Java 垃圾回收过程,老年代是实例生命周期的最后阶段。Major GC 扫描老年代的垃圾回收过程。如果实例不再被引用,那么它们会被标记为回收,否则它们会继续留在老年代中。

内存碎片:一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的 GC 进程中完成。

垃圾回收中实例的终结

在释放一个实例和回收内存空间之前,Java 垃圾回收器会调用实例各自的 finalize() 方法,从而该实例有机会释放所持有的资源。虽然可以保证 finalize() 会在回收内存空间之前被调用,但是没有指定的顺序和时间。多个实例间的顺序是无法被预知,甚至可能会并行发生。程序不应该预先调整实例之间的顺序并使用 finalize() 方法回收资源。

  • 任何在 finalize 过程中未被捕获的异常会自动被忽略,然后该实例的 finalize 过程被取消。
  • JVM 规范中并没有讨论关于弱引用的垃圾回收机制,也没有很明确的要求。具体的实现都由实现方决定。
  • 垃圾回收是由一个守护线程完成的。

对象什么时候符合垃圾回收的条件?

  • 所有实例都没有活动线程访问。
  • 没有被其他任何实例访问的循环引用实例。

Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

引用类型 垃圾收集
强引用(Strong Reference) 不符合垃圾收集
软引用(Soft Reference) 垃圾收集可能会执行,但会作为最后的选择
弱引用(Weak Reference) 符合垃圾收集
虚引用(Phantom Reference) 符合垃圾收集

在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。

12345678910 class Animal {`public static void main(String[] args) {Animal lion = new Animal();System.out.println(“Main is completed.”);}protected` `void` `finalize() {System.out.println("Rest in Peace!");}}`

在上面的类中,lion 对象在实例化行后从未被使用过。因此 Java 编译器作为一种优化措施可以直接在实例化行后赋值lion = null。因此,即使在 SOP 输出之前,finalize 函数也能够打印出 'Rest in Peace!'。我们不能证明这确定会发生,因为它依赖 JVM 的实现方式和运行时使用的内存。然而,我们还能学习到一点:如果编译器看到该实例在未来再也不会被引用,能够选择并提早释放实例空间。

  • 关于对象什么时候符合垃圾回收有一个更好的例子。实例的所有属性能被存储在寄存器中,随后寄存器将被访问并读取内容。无一例外,这些值将被写回到实例中。虽然这些值在将来能被使用,这个实例仍然能被标记为符合垃圾回收。这是一个很经典的例子,不是吗?
  • 当被赋值为 null 时,这是很简单的一个符合垃圾回收的示例。当然,复杂的情况可以像上面的几点。这是由 JVM 实现者所做的选择。目的是留下尽可能小的内存占用,加快响应速度,提高吞吐量。为了实现这一目标,JVM 的实现者可以选择一个更好的方案或算法在垃圾回收过程中回收内存空间。
  • 当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。

GC Scope 示例程序

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 Class GCScope {`GCScope t;static` `int` `i = 1;public static void main(String args[]) {GCScope t1 = new GCScope();GCScope t2 = new GCScope();GCScope t3 = new GCScope();// No Object Is Eligible for GCt1.t = t2; // No Object Is Eligible for GCt2.t = t3; // No Object Is Eligible for GCt3.t = t1; // No Object Is Eligible for GCt1 = null;// No Object Is Eligible for GC (t3.t still has a reference to t1)t2 = null;// No Object Is Eligible for GC (t3.t.t still has a reference to t2)t3 = null;// All the 3 Object Is Eligible for GC (None of them have a reference.// only the variable t of the objects are referring each other in a// rounded fashion forming the Island of objects with out any external// reference)}protected void finalize() {System.out.println(“Garbage collected from object” + i);i++;}class` `GCScope {GCScope t;static` `int` `i = 1;public static void main(String args[]) {GCScope t1 = new GCScope();GCScope t2 = new GCScope();GCScope t3 = new GCScope();// 没有对象符合 GCt1.t = t2; // 没有对象符合 GCt2.t = t3; // 没有对象符合 GCt3.t = t1; // 没有对象符合 GCt1 = null;// 没有对象符合 GC (t3.t 仍然有一个到 t1 的引用)t2 = null;// 没有对象符合 GC (t3.t.t 仍然有一个到 t2 的引用)t3 = null;// 所有三个对象都符合 GC (它们中没有一个拥有引用。// 只有各对象的变量 t 还指向了彼此,// 形成了一个由对象组成的环形的岛,而没有任何外部的引用。)}protected` `void` `finalize() {System.out.println("Garbage collected from object"` `+ i);i++;`}

JVM GC 算法

在判断哪些内存需要回收和什么时候回收用到 GC 算法,本文主要对 GC 算法进行讲解。

JVM 垃圾判定算法

常见的 JVM 垃圾判定算法包括:引用计数算法、可达性分析算法。

引用计数算法(Reference Counting)

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

优点:简单,高效,现在的 objective- c 用的就是这种算法。

缺点:很难处理循环引用,相互引用的两个对象则无法释放。因此目前主流的 Java 虚拟机都摒弃掉了这种算法。

举个简单的例子,对象 objA 和 objB 都有字段 instance,赋值令 objA.instance=objB 及 objB.instance=objA,除此之外,这两个对象没有任何引用,实际上这两个对象已经不可能再被访问,但是因为互相引用,导致它们的引用计数都不为 0,因此引用计数算法无法通知 GC 收集器回收它们。


public class ReferenceCountingGC {
    public Object instance = null;

    public static void main(String[] args) {ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();//GC}
}

运行结果


[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] 
[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
  eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000)
  from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
  to   space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
 ParOldGen       total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
  object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000)
 Metaspace       used 3469K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

从运行结果看,GC 日志中包含“3329K->744K”, 意味着虚拟机并没有因为这两个对象互相引用就不回收它们,说明虚拟机不是通过引用技术算法来判断对象是否存活的。

可达性分析算法(根搜索算法)

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。

从 GC Roots(每种具体实现对 GC Roots 有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。

在 Java 语言中,可以作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中 JNI(Native 方法)的引用对象

真正标记以为对象为可回收状态至少要标记两次。

四种引用

强引用就是指在程序代码之中普遍存在的,类似 ”Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。


Object obj = new Object();

软引用是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,提供了 SoftReference 类来实现软引用。


Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了 WeakReference 类来实现弱引用。


Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);

虚引用也成为幽灵引用或者幻影引用,它是最弱的一中引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK1.2 之后,提供给了 PhantomReference 类来实现虚引用。


Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);

JVM 垃圾回收算法

常见的垃圾回收算法包括:标记 - 清除算法,复制算法,标记 - 整理算法,分代收集算法。

在介绍 JVM 垃圾回收算法前,先介绍一个概念。

Stop-the-World

Stop-the-world 意味着 JVM 由于要执行 GC 而停止了应用程序的执行,并且这种情形会在任何一种 GC 算法中发生。当 Stop-the-world 发生时,除了 GC 所需的线程以外,所有线程都处于等待状态直到 GC 任务完成。事实上,GC 优化很多时候就是指减少 Stop-the-world 发生的时间,从而使系统具有高吞吐、低停顿的特点。

标记—清除算法(Mark-Sweep)

之所以说标记 / 清除算法是几种 GC 算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。标记 / 清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的 GC Roots 对象,对从 GC Roots 对象可达的对象都打上一个标识,一般是在对象的 header 中,将其记录为可达对象;

清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象 header 信息),则将其回收。

不足:

  • 标记和清除过程效率都不高
  • 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。

复制算法(Copying)

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。

不足:

  • 将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。
  • 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

标记—整理算法(Mark-Compact)

标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,因此其不会产生内存碎片。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

不足:

效率不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。

分代收集算法(Generational Collection)

分代回收算法实际上是把复制算法和标记整理法的结合,并不是真正一个新的算法,一般分为:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。

新生代:由于新生代产生很多临时对象,大量对象需要进行回收,所以采用复制算法是最高效的。

老年代:回收的对象很少,都是经过几次标记后都不是可回收的状态转移到老年代的,所以仅有少量对象需要回收,故采用标记清除或者标记整理算法。

正文完
 0