浅谈垃圾回收

引言

一直都想写一篇博客结合前辈的经验去阐述自己对垃圾回收的理解,但阿导本人经验尚欠,怕写的不好,所以拖了很久很久,文章中不足之处,还请多多包涵。

垃圾回收的背景

垃圾回收(Garbage collection),简称 GC,很多人都认为它是伴随 JAVA 的衍生物,其实不然,1960年Lisp这门语言中就使用了内存动态分配和垃圾回收技术,GC 比 JAVA (1995年诞生)要久远的多。

垃圾回收的策略

JVM 的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。
其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而 Java 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
我们常见的垃圾回收策略有如下两种,下面允许我简要介绍一波。

引用计数法

这种垃圾回收策略是比较早的一种策略,这种回收策略就是堆中对象实例有一个引用计数,当对象被创建,该引用计数便会加 1,但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

下面分析一下这种垃圾回收的优缺点

  • 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
  • 缺点:无法检测到循环引用,因为他们引用计数永远不会为0导致其无法进行垃圾回收。

引用计数法

可达性分析法

可达性分析法来源于离散数学的图论,程序会把引用关系看做一张图,从 GC Root 节点出发,向下找出所有被引用的节点,那些没有找到的节点就会被当成垃圾进行回收。
一般在 java 中可以作为 GC Root 的对象如下:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(Native方法)引用的对象
    可达性分析法
    在可达性分析法中不可达的对象,也不一定会被回收,这时候它们暂时处于第一次标记阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
  • 第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;
  • 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。
  • 第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

方法区存储内容是否需要回收的判断可就不一样了。方法区主要回收的内容有:废弃常量和无用的类。

  • 对于废弃常量也可通过引用的可达性来判断,

  • 对于无用的类则需要同时满足下面3个条件:

      该类所有的实例都已经被回收,即 Java 堆中不存在该类的任何实例
      加载该类的 ClassLoader 已经被回收
      该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    

常见的垃圾回收算法

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

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

标记清除算法

复制算法(Copying)

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于 copying 算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
复制算法

标记整理算法(Mark-Compact)

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:
标记整理算法

分代收集算法

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
分代收集算法
分代收集算法

  • 年轻代(Young Generation):主要以复制算法进行垃圾回收
  1. 年轻代存放所有新生成的对象。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

  2. 新生代内存按照 8:1:1 的比例分为一个 Eden 区和两个 Survivor(Survivor0,Survivor1) 区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。回收时先将 Eden 区存活对象复制到一个 Survivor0 区,然后清空 Eden 区,当这个 Survivor0 区也存放满了时,则将 Eden 区和 Survivor0 区存活对象复制到另一个 Survivor1 区,然后清空 Eden 和这个 Survivor0 区,此时 Survivor0 区是空的,然后将 Survivor0 区和 Survivor1 区交换,即保持 Survivor1 区为空, 如此往复。

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

  4. 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高,但不一定等 Eden 区满了才触发。

  • 年老代(Tenured Generation):主要以标记-整理算法进行垃圾回收
  1. 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

  2. 内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

  • 永久代(Permanet Generation)

用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

常见的垃圾回收器对比

垃圾回收器回收器使用的算法回收器简介
Serial复制算法新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是 client 级别默认的 GC 方式,可以通过 -XX:+UseSerialGC 来强制指定。
Serial Old标记-整理算法老年代单线程收集器,Serial 收集器的老年代版本。
ParNew停止-复制算法新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现
Parallel Scavenge停止-复制算法并行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC 线程时间)。适合后台应用等对交互相应要求不高的场景。是 server 级别默认采用的 GC 方式,可用 -XX:+UseParallelGC 来强制指定,用-XX:ParallelGCThreads=4 来指定线程数。
Parallel Old停止-复制算法Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。
CMS(Concurrent Mark Sweep)标记-清理算法高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。
G1Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器机器、大内存机器。它高度符合垃圾收集暂停时间的目标,同时实现高吞吐量。Oracle JDK 7 update 4 以及更新发布版完全支持 G1 垃圾收集器。

Hotspot 虚拟机包含的垃圾回收器如下:
Hotspot 虚拟机包含的垃圾回收器

垃圾回收在 JVM 中何时触发

因为垃圾回收做了分代处理,所以垃圾回收区域和时间也不一样,GC 有两种类型:Scavenge GC 和 Full GC。

Scavenge GC

在心对象生成的时候,会在 Eden 区申请空间,若空间申请失败便触发 Scavenge GC,对 Eden 区域进行 Scavenge GC,清除非存活的对象,并将存活的对象转移到 Survivor 区,然后整理两个 Survivor 区。因为 Scavenge GC 是在 Eden 区进行的,不会影响到年老代,大部分对象都是从 Eden 区开始的,而 Eden 区空间一般不会分配很大,所以 Eden 区的 GC 很频繁,一般在该区域需要使用高效快速的回收算法,让 Eden 区域快速空闲出来。

Full GC

Full GC 是对整个堆区进行 GC ,包括年轻代、年老代和永久代,因此要比 Scavenge GC 要慢,因此尽可能的减少 Full GC 的次数,在 JVM 调优过程中,基本上都是在对 Full GC 进行调节,一般触发 Full GC 的情况如下:

  • 年老代区域被写满
  • 永久代区域被写满
  • System.gc() 被调用
  • 上一次 GC 之后 Heap 的各域分配策略动态变化

需要云服务器的不要错过优惠

阿里云低价购买云服务,值得一看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值