天天看点

深入理解JAVA虚拟机-垃圾收集器GC

  1. JAVA主要的几种垃圾收集器。
  2. 主要的垃圾收集算法。
  3. 内存分配策略:哪些内存需要回收,什么时候回收,怎么回收。

Java 与C/C++相比,最大的特点就是JAVA引入了自动垃圾回收,它解决了 C/C++ 最令人头疼的内存管理问题,让程序员专注于程序本身,不用关心内存回收这些恼人的问题。每个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,程序只有在运行的期间才知道创建了多少个对象,这部分的内存分配和回收都是动态的。垃圾收集器所关注的也就是这部分内存。

要搞懂垃圾回收的机制,我们首先要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域。

深入理解JAVA虚拟机-垃圾收集器GC

JAVA虚拟机运行时数据区

JAVA虚拟机各内存区域的描述和作用可以查阅上一篇文章。

程序计数器,虚拟机栈和本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每一栈帧中分配的内存基本上在类的结构确定下来时就是已知的,因此这三个区域的内存分配和回收都是具备确定性的。方法结束或线程结束时,内存自然就跟随着回收。而JAVA堆不一样,程序只有在运行期间才知道创建了哪些对象,这部分的内存分配和回收都是动态的。

  1. 那么如何识别哪些内存需要回收?
  • 引用计数算法
  • 可达性分析算法

引用计数算法:

简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收。

String ref = new String("Java");   // ref引用了右边的字符串对象,所以ref的引用次数是1           

引用计数算法实现简单,判定效率也高,但主流的JAVA虚拟机没有选用此引用计数算法来管理内存,其最主要的原因是很难解决对象之间的相互循环引用问题。

public class ReferenceCountingGC {
  
  public Object instance = null;
  
  public void testGC() {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    
    objA.instance = objB;
    objB.instance = objA;
    
    objA = null;
    objB = null;
    
    // 假设这里发生GC,能否回收objA和objB
    System.gc();
  }
}           

可达性分析算法:

在主流的虚拟机实现中都是通过可达性分析来判定对象是否存活。这个算法的基本思路就是通过一系列的GC Roots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则说明此对象是不可用的。

深入理解JAVA虚拟机-垃圾收集器GC

可达性分析算法判定对象是否存活

在JAVA中,可以作为GC Roots的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。
  1. 垃圾回收主要方法

可以通过可达性算法来识别哪些数据是垃圾,那该怎么对这些垃圾进行回收呢。主要有以下几种方式方

  • 标记清除算法
  • 复制算法
  • 标记整理法
  • 分代收集算法

标记清除算法

最基础的收集算法:标记-清除算法。分为“标记”和“清除”两个阶段。

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一

个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后

在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次

垃圾收集动作。

复制算法

为了解决效率问题,一种称为“复制”的收集算法出现,它将可用内存一分为二大小相等的两块,每次只用其中的一块,当这一块内存用完,就将还存活的对象复制到另一块内存,然后把已使用过的内存空间一次清除掉。这样使得每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片问题,只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点是将内存一分为二,可用的内存只有一半。

标记整理法

前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

分代收集算法

当前虚拟机的垃圾收集都采用“分代收集”算法。它根据对象存活周期的不同将内存划分为几块,一般是把JAVA堆内存分为新生代和老年代。这样就可以根据各个年代的特点采用最适合的收集算法。

新生代:每次垃圾收集时都有大量的对象死去,只有少量的存活。适用复制算法。

老年代:对象存活率高,没有额外的空间进行担保,必须选用“标记-清除” 或“标记-整理”算法进行回收。

垃圾收集器

收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。JAVA虚拟机规范中对垃圾收集器应该如何实现没有任何规定,不同的虚拟机提供的垃圾收集器可能有很大的差别。

主要收集器:

深入理解JAVA虚拟机-垃圾收集器GC

虚拟机的垃圾收集器

  • 新生代垃圾回收器:Serial, ParNew, ParallelScavenge
  • 老年代垃圾回收器:CMS,Serial Old, Parallel Old
  • 同时在新老生代垃圾回收器:G1

Serial 收集器

Serial 收集器是最基本,发展历史最悠久的收集器。它是一个单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,其他像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成一样。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一个新生代收集器,使用复制算法,多线程,功能和 ParNew 收集器一样。与其他收集器不同的关注点是Parallel Scavenge 收集器目标是达到一个可控制的吞吐量。

Serial Old 收集器

Serial 收集器是工作于新生代的单线程收集器,与之相对地,Serial Old 是工作于老年代的单线程收集器,使用“标记-整理”算法。

Parallel Old 收集器

Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,真正实现了「吞吐量优先」的目标。

CMS 收集器

CMS 收集器是一种以获取最短回收停顿时间为目标的收集器,注重服务的响应速度,系统的最短停顿时间,以给用户带来较好的体验。CMS收集器也基于“标记-清除”算法。

G1(Garbage First) 收集器

G1是当前主流的垃圾收集器,是一款面向服务端应用的垃圾收集器。与其他收集器相比,G1具备几大特点:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测停顿

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Regn),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

总结

内存回收与垃圾收集器在很多时候都是影响系统性能,并发能力的主要因素之一,只有根据实际需求,实现方式选择最优的收集方式才能获取最高的性能。

继续阅读