Java虚拟机是如何进行垃圾回收的?

1.概述

在Java运行时数据区域的各个部分中,程序计数器、Java虚拟机栈、本地方法栈这3个内存区域的生命周期与线程的生命周期一致,与线程共生死。栈中的栈帧随着方法的进入和退出而执行着入栈和出栈的操作,每一个栈帧需要分配多大的内存空间基本上是在类结构确定下来时就是已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多的考虑内存回收的问题,需要注意的是方法的递归调用时的出口问题,避免造成StackOverflowError。

而Java堆和方法区这两个内存区域,需要处于运行期间,我们才知道这部分内存区域的内存是如何进行分配的,因此垃圾收集器所关注的也是这部分区域的内存。

2.垃圾回收

在Java堆中几乎存放着所有的对象实例,垃圾收集器在对堆内存进行回收时,首先需要确认哪些对象还在被使用,哪些对象不再被使用了。那么我们需要怎么来确定对象的状态呢?

2.1 获取对象引用状态

对象状态?存活 or 死去

2.1.1 引用计数法

在对象中添加一个引用计数器,当其被引用时,计数器的值就+1,当引用失效时,计数器值就-1,任何时刻引用计数器的值为0的对象就是不再被使用的对象。

如果存在两个对象互相引用着对方,除此之外,这两个对象不被其他地方所引用,实际上这两个对象已经不可能再被访问,但引用计数器的值都为1,使用引用计数法来判断对象引用状态时无法对进行回收。

此时两个ReferenceCountingObject的对象被强引用,因此不会被垃圾垃圾收集

[0.224s][info][gc] GC(4) Pause Full (System.gc()) 17M->17M(20M) 2.517ms

/**
 * Vm options:-Xmx20m
 */
public class ReferenceCountingObject {

    private ReferenceCountingObject instance;

    private byte[] largeObject = new byte[5 * 1024 * 1024];

    public static void main(String[] args) {
        ReferenceCountingObject objA = new ReferenceCountingObject();
        ReferenceCountingObject objB = new ReferenceCountingObject();
        System.gc();
    }
}

取消对两个ReferenceCountingObject的对象的强引用,并让其中的instance变量互相引用对方,两个ReferenceCountingObject对象在垃圾收集阶段被回收,说明Java虚拟机在垃圾收集时使用的不是引用计数算法。

[0.201s][info][gc] GC(4) Pause Full (System.gc()) 17M->5M(20M) 2.963ms

/**
 * Vm options:-Xmx20m
 */
public class ReferenceCountingObject {

    private ReferenceCountingObject instance;

    private byte[] largeObject = new byte[5 * 1024 * 1024];

    public static void main(String[] args) {
        ReferenceCountingObject objA = new ReferenceCountingObject();
        ReferenceCountingObject objB = new ReferenceCountingObject();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.gc();
    }
}

2.1.2 可达性分析法

首先定义一系列的GC Roots根对象,将这些根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到根对象之间没有任何引用链相连,则表明此对象不再被使用。

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

  • 在虚拟机栈的栈帧中的局部变量表中引用的对象,例如方法参数、局部变量等
  • 在本地方法栈中JNI引用的对象
  • 在方法区中静态属性引用的对象
  • 字符串常量池的里的引用的对象
  • 所有被synchronized关键字持有的对象

可达性分析算法

2.1.3 引用分类

Java中的引用分为强引用(Strongly Reference)软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这4种引用强度依次减弱。

Java引用

2.2 垃圾收集算法

当前虚拟机的垃圾收集器,大多数遵循“分代收集理论”来进行设计,而“分代收集理论”又是建立在两个分代假说之上。

分代收集理论

2.2.1 分代收集理论

基于“分代收集理论”设计的垃圾收集器遵循一个原则:收集器应该将Java堆分成不同的内存区域,根据对象的年龄将其分配到这些区域中去。

既然说到根据对象的年龄划分内存区域,一般来说,Java虚拟机至少会把Java堆划分为新生代(Young Generation)老年代(Old Generation)两个区域。

在新生代中,每次垃圾收集都会收集掉大量的对象,而存活下来的对象会随着年龄的增长逐步晋升到老年代中。

在对Java堆划分出不同的区域之后,垃圾收集器才能够针对不同的区域采取不同的垃圾收集方案,因此产生了Minor GC(新生代的垃圾收集)Major GC(老年代的垃圾收集)Full GC(整个Java堆和方法区的垃圾收集)这样的收集类型,也才能够根据不同内存区域中存储对象的特征匹配不同的垃圾收集算法(标记-复制算法标记-清除算法标记-整理算法)。

2.2.2 标记-清除算法

垃圾收集时,首先标记出需要收集的对象,并收集。反过来,标记存活的对象,收集未被标记的对象。

标记-清除算法是最基础的收集算法,它的主要缺点是:

标记、清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致需要分配较大对象时无法找到足够的连续内存空间从而频繁触发垃圾收集,降低系统运行效率。

标记-清除算法

2.2.3 标记-复制算法

为了解决标记-清除算法存在的内存碎片的问题,标记-复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块内存,垃圾收集时,把正在使用的这块内存中存活的对象复制到未使用的另一块内存中去,然后清理掉正在使用的这块内存中的所有对象,接着交换两块内存的角色,等待下一次垃圾收集。它的主要缺点是:

  • 如果正在使用的这块内存中多数对象是存活的,就会产生大量的内存间对象复制的开销。

  • 内存使用率低。

标记-复制算法

2.2.4 标记-整理算法

标记-复制算法虽然解决了内存碎片的问题,但同时也造成了一部分内存空间的浪费,如果对象存活率比较高,则会产生大量的内存间对象复制的开销。

标记-整理算法的标记过程与标记-清除算法的标记过程一样,但不是在标记完成后,直接清除可回收对象,而是将所有存活的对象向内存空间的一端移动,然后清理掉边界以外的内存。

标记-整理算法

如果移动存活对象,尤其是在老年代这种每次垃圾回收都有大量对象存活的内存区域,移动存活对象并更新所有引用这些对象的地方将会是一种开销极大的操作,而且这种对象移动操作必须全程暂停用户程序才能进行,这种暂停被描述为Stop The World

2.2.5 三种算法对比

垃圾收集算法优点缺点
标记-清除算法实现起来简单产生内存碎片,影响分配内存的速度
标记-复制算法不存在内存碎片内存使用率低;存在大量存活对象时,复制开销大
标记-整理算法不存在内存碎片整理对象开销大

2.2.6 垃圾收集算法细节

垃圾收集算法实现

2.3 垃圾收集器

JVM垃圾收集器

2.3.1 Serial

Serial收集器是最基础、历史最悠久的垃圾收集器。它是一个单线程工作的新生代收集器,采用标记-复制算法。

Serial收集器

2.3.2 ParNew收集器

ParNew收集器是Serial收集器的多线程并行版本,是一个新生代收集器,可以同时使用多条线程进行垃圾收集。

ParNew收集器可以配合Serial Old收集器,完成对新生代和老年代的垃圾收集工作。

ParNew/Serial Old收集器

2.3.3 Parallel Scanvenge收集器

使用标记-复制算法,是一个注重吞吐量的多线程垃圾收集器,作用于新生代。

吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间)

2.3.4 Parallel Old垃圾收集器

Parallel Scanvenge收集器的老年代版本,基于标记-整理算法。

2.3.5 Serial Old收集器

Serial收集器的老年代版本,基于标记-整理算法。

2.3.5 CMS收集器

CMS(Concurrent Mark Sweep)收集器即并发标记清除收集器,是一个老年代收集器,从名字上可以看出,其是基于标记-清除算法来实现的。

其垃圾收集过程分为4个步骤:

  1. 初始标记

    初始标记阶段会触发Stop The World,仅标记GC Roots能直接关联到的对象,速度很快

  2. 并发标记

    并发标记阶段会从GC Roots的直接关联对象向下标记所有可以关联的对象,该过程耗时较长,但不会触发Stop The World,可以与用户线程并发运行

  3. 重新标记

    重新标记阶段会触发Stop The World,该阶段会修正并发标记阶段因用户程序继续运行而导致标记发生变动的那一部分的标记记录

  4. 并发清除

    并发清除阶段会清除掉标记阶段标记为需要清理的对象,由于采用标记-清除算法,不需要移动存活的对象,所以该阶段可以与用户线程并发运行

CMS收集器

CMS收集器的优点:

  • 并发收集

  • 低停顿

CMS收集器的缺点:

  • 对CPU资源消耗大,不适合CPU少的机器使用;
  • 无法处理浮动垃圾,在并发清理的过程中,用户线程也在继续运行,自然就伴随着新的可回收对象产生,CMS无法在此次垃圾收集过程中清理掉它们;
  • 会产生内存碎片

2.3.4 Garbage First收集器

Garbage First收集器,简称G1收集器,在G1收集器之前出现的所有其他收集器,包括CMS收集器,都是遵循分代收集理论来设计的,它们的收集范围要么是新生代(Minor GC),要么是老年代(Major GC),而G1则可以面向堆内存的任何部分进行收集。

G1收集器将Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以是新生代的Eden区、Survivor区或者老年代空间。每个Region的大小可以通过参数-XX:G1HeapRegionSize来设定,取值范围为1MB-32MB,根据这个数字,我们可以计算出G1收集器将Java堆划分出了多少个Region。

Region中还有一类特殊的特殊的Humongous区域,专门用来存储大对象。对于大对象的定义:对象所占的内存超过Region容量的一半。

G1收集器会根据各个Region中垃圾堆积的价值大小,优先处理回收价值最大的那些Region,以便在有限的时间内获取尽可能高的垃圾收集效率。

G1收集器垃圾收集的四个步骤:

  1. 初始标记

    标记GC Roots根节点能直接关联到的对象,这个阶段会暂停用户线程,但耗时很短

  2. 并发标记

    从GC Roots根节点开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,这个阶段耗时较长,但可与用户线程并发执行

  3. 最终标记

    对用户线程做短暂的暂停,用于处理并发标记阶段遗留的对象

  4. 筛选回收

    负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定收集计划,把决定回收的那一部分Region中存活的对象复制到空的Region中,再清理掉整个旧Region的全部空间。这个阶段涉及到对象的移动,因此必须暂停用户线程,由多条收集器线程并行完成

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值