JVM垃圾收集

仅供学习交流,如有错误请指出,如要转载请加上出处,谢谢

JVM垃圾收集技术需要关注三个步骤:1.哪些对象需要被回收,2.什么时候进行回收,3.如何进行回收

哪些对象需要被回收

引用计数算法

引用技术算法大致的过程是给对象分配一个引用计数器,当该对象被引用时,引用计数器就加一,该引用失效时,计数器就减一,当计数器为零时,说明该对象没有任何引用,就判定该对象可以被回收,虽然它的实现简单即高效,不过现代主流的JVM收集器都没有使用该算法,它有一个致命的缺陷是无法解决对象之间相互循环引用的问题

可达性分析算法

可达性分析算法大致的过程是通过一系列的成为GC Roots的对象作为起点,他会向下进行搜索,所走过的路径成为引用链,如果一个对象到GC Roots没有任何的引用链(指该对象在程序中没有任何关系了),则说明该对象是可以被回收的
在java语言中,可以作为GC Roots的对象包括如下:

  1. 虚拟机栈中的栈帧内的本地变量表中引用的对象
  2. 方法区中类静态属性引用和常量引用的对象
  3. 本地方法栈中引用的对象

什么时候进行回收

在现代的内存分配中,堆被分为年轻代(8:1比例分配的1个Eden区和2个Survivor(FromSurvivor 和 ToSurvivor))和老年代,那什么时候会对年轻代和老年代的对象进行回收呢?

首先,创建一个对象,JVM会给该对象分配一个对象年龄计数器,该对象大部分情况下会被优先分配到年轻代中的Eden区中(对于大对象,直接分配到老年代中),当Eden区中没有被分配的空间时,这时候JVM会触发一次Minor GC(年轻代GC),该对象的年龄置为1,存活下来的对象会往ToSurvivor区中移动,FromSurvivor区中存活的对象也复制到ToSurvivor区中,这时,ToSurvivor区和FromSurvivor区身份调换,当该对象的年龄增加到一定程度(默认是15)时,该对象会被晋升到老年代中,当然如果FromSurvivor中的相同年龄段的对象超过了一般,则大于该年龄段的对象直接晋升到老年代,在老年代中如果最大可用的连续空间小于晋升到老年代的对象大小,会进行一次Full GC

如何进行回收

垃圾收集算法

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

标记-清除算法的回收过程分为两个阶段:标记和清除,标记就是标记哪些对象可以被回收(上面已经介绍过了),清除就是回收哪些被标记的可回收对象,这两个阶段的效率都不是很高,而且会产生大量的空间碎片,过程如下图

复制算法(Copying)

复制算法的回收过程是将内存分成两块等量大小的内存块,一块是存储对象数据,另一块是保留区域(不存储任何数据),当存储对象数据的内存块用完了,就将还存活的对象复制到保留区域,然后将已使用过的内存块清理掉,原来存储对象数据的内存块变成了保留区,原来的保留区变成了存储对象数据的内存块,复制过程只需要移动堆顶的指针可以,简单高效,而且没有内存碎片化的存在,但是却将原有的内存缩小了一般,这也适合现代新生代的回收机制,因为新生代大部分都是朝生夕死,当然他们的内存划分比例不需要到达1:1,典型的就是8:1的eden区和survivor区,具体过程如下图:

标记-整理算法

标记-整理算法的回收过程分为标记和整理两个阶段,它是标记-清除算法的改进版本,标记和标记-清除算法一致,不同的是,他不是直接对可回收的对象进行清除,而是将存活下来的对象往一端移动,再清理掉存活边界外的内存,它解决了标记-清除算法的内存碎片化问题,但是效率不是很高,具体过程如下图:

分代收集算法

分代收集算法是指按照对象的存活周期,在堆中分为新生代和老年代,根据不同年代的特点使用不同收集算法的组合,比如新生代的特点是朝生夕死,对象存活周期短,使用复制算法,只需要少量的存活对象的复制成本即可,而老年代存活周期长,使用标记-清除或者标记-整理算法,因为它们的内存不大,没有额外的空间进行担保,这样就会形成一个组合来进行垃圾收集

垃圾收集器

JVM的发展中,发布了很多的收集器,从最开始的单线程版的收集器serial(针对新生代)/serial old(针对老年代)收集器(JDK1.3以前)到多线程的并行收集器parallel scavenge(针对新生代)/parallel old(针对老年代)收集器,一直到现在针对多核,多CPU的环境下,充分的利用其硬件资源和重视快速响应的CMS收集器(JDK1.5以后)和G1收集器(JDK1.7以后)

CMS收集器

cms(Concurrent Mark Sweep)收集器是一款针对于最短回收时间停顿的老年代收集器,从名称上就可以知道该收集是基于标记-清除算法,它是把最耗时间的标记(GC Root Tracing)和清除过程使用了并发机制,和用户线程共同运行,大大的减少了停顿时间,具体过程如下:

该收集器可以分为五个步骤:

  1. 初始标记:标记GC Roots所能关联的对象
  2. 并发标记:根据第一步的对象并发的遍历其他的对象(GC Roots Tracing)
  3. 重新标记:由于第二步的运行时间较长,对象可能会产生变化,开启多个线程重新标记已标记的对象
  4. 并发清理:并发的从需要被收集的对象集合中清除这些对象
  5. 并发重置:重置CMS收集器的数据,为下一次收集做准备

上述的五大步骤就是CMS收集的过程,整的来说已经实现了并发收集,低停顿,响应快等优点,但是还有三个明显的缺点:

  1. 由于CMS收集器是并发收集,它会占用其他线程的CPU资源,导致吞吐量低,部分线程变慢
  2. CMS收集器无法处理在重新标记这个时间段里的垃圾,因为在重新标记期间,程序会产生新的对象或者变动,这是CMS收集器会预先预留一部分内存来处理,所以在一定的比例下(默认老年代空间使用率到达68%),就会触发CMS收集
  3. CMS收集器采用的是标记-清除算法,所以会产生大量的内存碎片

G1收集器

G1收集器(Garbage-First)是一款服务器型的垃圾收集器,它是由一个个大小相等的Regoin(内存区域)组成,通过一系列的标记阶段之后,之后优先收集那些垃圾最多的区域,它也保存了以往的分代收集的概念,但是新生代和老年代不需要设定固定的大小来控制,这样在内存的使用上提供了很大的灵活性,传统的堆分区如下:

上述的是传统的把堆分成年轻代(1个eden区和2个survivor区),老年代,和永久代,在G1收集器中也保持了分代的理念,如下图:

如上图所述,他是用一个个内存区域的概念来存储分代对象,和CMS收集器不同的是它是用标记-整体算法来进行收集过程,整体的解决了内存碎片的问题

参考

http://zhaoyanblog.com/archives/397.html
https://book.douban.com/subject/24722612/