由浅入深理解JVM垃圾回收机制


前言

  在很长的一段时间里,我一度认为JVM的底层知识宛如夜空中忽明忽暗的星星一般,遥远又神秘,以至于我对其百般好奇却又望而止步。然而,知识的那团乌云久踞头顶上空,让我感到惶恐焦虑。终于我决定,摒弃执念,追求真理。翻译翻译——什么叫TMD垃圾回收机制!

1 什么是垃圾

  众所周知,Java相对于C++而言,自动垃圾回收机制是其广受青睐的原因之一。一般情况下,确定一个对象是否为可回收的垃圾有引用计数法和根可达法两种方法。

1.1 引用计数法

  每个对象维护一个引用计数器,当引用数为0时,即表明该对象为可回收对象。但是引用计数法无法定位互相引用的垃圾,比如A对象中引用了B,B对象中引用了A,这两个对象除了互相引用之外并没有其他引用,那么这两个对象其实都是可回收对象。

1.2 根可达算法

  通过GC Roots可以定位到的对象为存活对象,其余对象为可回收对象。GC Roots包括四种对象:虚拟机栈中的引用的对象、全局的静态的对象、常量引用和本地方法栈中JNI引用的对象。

2 常见的垃圾回收算法

  常见的垃圾回收算法有三种,标记清除(Mark-Sweep)、复制(Copying)和标记压缩(Mark-Compact)。

2.1 标记清除

  标记清除需要对所有对象进行扫描并将可回收对象标记出来,然后再次扫描,直接将可回收对象所占的内存空间进行擦除。
在这里插入图片描述
  标记清除算法的优点是算法简单,在存活对象多的情况下效率高,只需要将少部分的垃圾清除即可。其缺点是需要扫描两次内存空间,并且将垃圾清除后容易产生内存碎片。

2.2 复制

  复制算法将内存分为两块大小相等的区域A和B,一开始只使用A存放对象。垃圾回收时,扫描A区域并将不可回收的存活对象移动到B区域,将所有存活对象移动到B区域之后,将整个A区域进行擦除,后续创建对象将在B区域分配内存。
在这里插入图片描述
  复制算法的优点是算法简单,只需要扫描一次内存空间,并且不会产生内存碎片。其缺点是每次只能使用一半内存造成空间浪费,并且垃圾回收过程中需要移动对象。

2.3 标记压缩

  标记压缩算法先要对整个内存区域进行扫描,标记出可回收对象。然后再次扫描,将不可回收对象往内存的一边移动。
在这里插入图片描述
  标记压缩算法的优点是不会产生内存碎片,并可以充分利用整个内存空间。其缺点是需要扫描两次内存空间,并且需要移动对象,效率偏低。

3 堆空间的区域划分

在这里插入图片描述
  如图,堆被划分为新生代和老年代两个区域,新生代又被划分为Eden区和两个Survivor区。这样划分的目的是为了更好地管理内存中的对象,并可以对不同的区域使用不用的垃圾回收算法(分代垃圾回收算法)。
  我们可以通过参数-Xms(堆初始化大小)和-Xmx(堆最大空间)来设定堆空间的大小,-Xms和-Xmx实际上是-XX:InitialHeapSize和-XX:MaxHeapSize的缩写。一般可以将两个值设为相等,这样可以得到一个固定大小的堆内存,方便开发和维护。年轻代和老年代空间大小比例默认为1 : 2,可以通过参数–XX:NewRatio来设定年轻代占整个堆内存的比例。Eden区和两个Survivor区的比例为8 : 1 : 1,可以通过参数–XX:SurvivorRatio来设定Survivor区占整个Eden区的比例。

4 对象在内存中的空间分配

  对象在内存中的分配过程如下:

  1. 即时编译器JIT对该对象进行逃逸分析,若没有发生逃逸,则直接在栈上分配。好处是方法结束之后,直接从栈中弹出,不需要GC。
  2. 如果对象很大,直接进入老年代。
  3. 如果TLAB(Thread Local Allocate Buffer)的剩余空间 > 对象所需的空间(tlab_top + size <= tlab_end),则在TLAB上直接分配对象。
  4. 判断TLAB的剩余空间 < refill_waste_limit,此时剩余空间允许浪费,创建新的TLAB并为对象分配空间。
  5. 如果TLAB的剩余空间 > refill_waste_limit,即TLAB还可以为其他的小对象分配空间。则从Eden区分配。
  6. 判断Eden区是否有足够空间存放对象,如果有,直接在Eden区分配。
  7. 如果Eden区没有足够的空间,执行一次Young GC(minor collection)。
    经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到Old区。

5 常见的垃圾回收器

在这里插入图片描述
  

5.1 Serial/Serial Old

  Serial是一个单线程垃圾回收器,使用复制算法,针对于年轻代的垃圾回收。对于限定单个CPU的环境来说,Serial回收器由于没有线程交互的开销,垃圾回收效率高。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
  Serial Old是Serial的老年代版本,其使用的是标记压缩算法。

5.2 Parallel Scavenge/Parallel Old

  Parallel Scavenge收集器与吞吐量关系密切,故也称为吞吐量优先收集器。Parallel是一个多线程收集器,使用复制算法,针对于年轻代的垃圾回收。该收集器的目标是达到一个可控制的吞吐量,通过参数-XX:GCRatio设置吞吐量的大小。Parallel Scavenge还可以开启GC自适应调节策略(与ParNew垃圾回收器最重要的区别),不需要手动指定新生代的大小、Eden区与Survivor区的比例、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
  Parallel Old是Parallel Scavenge的老年代版本,使用标记压缩算法。注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge和Parallel Old回收器组合使用。

5.3 CMS(Concurrent Mark Sweep)

5.3.1 CMS工作过程

  CMS是一个里程碑式的垃圾回收器,在JDK1.4后期引入,开创了垃圾并发回收(用户线程和垃圾回收线程同时工作)的先河。CMS是一种以获取最短回收停顿时间为目标的收集器,使用标记清除算法。CMS垃圾回收有四个步骤:初始标记、并发标记、重新标记和并发清除。
在这里插入图片描述
  在初始标记阶段,标记GC Roots能直接到达的对象,速度很快但是仍存在STW问题。在并发标记阶段,找出所有存活对象且用户线程可并发执行。在重新标记阶段,为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记,此阶段仍然存在STW。并发清除阶段,对标记的对象进行清除回收,同时用户线程可以并发执行。

5.3.2 CMS问题及解决方法

  CMS虽然缩短了STW,但是也存在缺陷。因为CMS使用标记清除算法,会产生内存碎片,导致大对象无法分配内存,提前引发Full GC。另外,CMS无法处理浮动垃圾,可能出现Concurrent Mode Failure而导致另一次Full GC的产生。
  当出现Concurrent Mode Failure时,往往都是由内存碎片导致的,决办法就是要让年老代留有足够的空间,以保证新对象空间的分配,具体做法是调整年轻代和老年代的比例,或者降低触发CMS的阀值(–XX:CMSInitiatingOccupancyFraction)。

5.3.3 并发标记算法

  CMS的并发标记阶段,使用了三色标记和增量更新算法。具体内容将在日后补充~

5.4 ParNew

  ParNew和Parallel Scavenge类似,不同之处在于,ParNew做了一些增强,可以和CMS配合工作。

5.5 G1

5.5.1 G1简介

  G1(Garbage First)垃圾,是一款面向服务端应用的垃圾收集器。与上面所述的几种垃圾回收器不同,Serial、Parallel Scavenge和ParNew针对于年轻代垃圾回收,CMS、Serial Old和Parallel Old针对于老年代的垃圾回收,而G1同时负责年轻代和老年代的回收。并且,G1只在逻辑上对堆内存进行分区,而上述六种垃圾回收器不仅在逻辑上对堆内存进行分区,物理上也会分区。

5.5.2 G1工作过程

  G1的工作过程与CMS类似,一共有四个步骤:初始标记、并发标记、重新标记和筛选回收。
在这里插入图片描述

5.5.3 G1特点

  • G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。
  • 分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
  • 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

6 垃圾收集器的选用

  除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别要求。使用VM提供给的默认GC就好)。如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:

  • 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+UseSerialGC的串行收集器。
  • 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+UseSerialGC的串行收集器。如果峰值应用程序性能是第一要务,并且没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
  • 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
  • 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
  • 如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)

  这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能仍然不足,尝试使用其他收集器。
  总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值