JVM GC垃圾回收

一、为什么要学习垃圾回收机制

日常开发中,我们并不太关心对象的回收和释放,因为这些工作都是JVM帮我们来做的。然而垃圾回收机制的掌握对于我们开发人员来说是十分重要的,虽然JVM帮助我们做这些工作,减轻了工作量,但是不合理的垃圾回收机制往往会导致系统性能上的瓶颈。

二、GC要做什么

  • 哪些内存需要回收?
  • 什么时候回收?
  • 怎么回收?

三、如何确定哪些对象已经死亡

1.引用计数法(Reference Counting)

  在Java中对象和引用是有关联的,如果要操作对象则必须用引用进行。因此可以通过引用计数来判断一个对象是否可以回收。

2.根搜索算法(GC Roots Trancing)

  为解决引用计数法中的循环引用。通过GC Roots对象作为起点搜索。如果和对象间没有可达路径,则该对象是不可达的。
  不可达对象不等于可回收对象。

四、垃圾收集算法

1.标记清除法(Mark-Sweep)

  分为标记和清除。
  标记:标记出所有需要回收的对象。
  回收:回收被标记对象所占用的空间。
缺点:内存碎片化严重,可能会产生大的对象找不到可利用的空间问题。

2.复制算法(copying)

  为解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。
按内存容量将内存划分为等大小的两块。每次只使用其中一块,这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
优点:实现简单,内存效率高,不易产生碎片。
缺点:可用内存被压缩到原来的一半。存活对象增多的话,Copying算法的效率会大大降低。

3.标记整理算法(Mark-Compact)

  标记阶段同Mark-Sweep算法。标记后将存活对象移向内存的一端。然后清除端边界外的对象。

4.分代收集算法(Generational Collecting)

  目前大多数JVM所采用的方法。
  核心思想:根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为(Tenured/Old Generation)和新生代(Yong Generation)。

  • 老生代特点:每次垃圾回收时只有少量对象需要被回收。
  • 新生代特点:每次垃圾回收时都有大量垃圾需要被回收。
    因此,可以根据不同区域选择不同算法。

5.增量收集算法

  在现有的收集算法中,每次垃圾回收,应用程序都会处于一种Stop the World的状态,这种状态下,应用程序所有的线程都会被挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或系统稳定性。如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替进行。每次,垃圾回收线程只收集一小片区域的内存空间,接着切换到用户线程继续执行。依次反复,知道垃圾收集完成。在垃圾回收过程中间断性地执行了应用程序代码,所以能减少系统的停顿时间,但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总成本上升,造成系统吞吐量的下降。

1) 新生代与复制算法

  目前大部分JVM的GC对于新生代都采用复制(Copying)算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。
  一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space,To Space),每次使用Eden空间和其中一块Survivor空间,当进行回收,将该两块空间中还存活的对象复制到另一块Survivor空间中。
在这里插入图片描述

2) 老年代与标记复制算法

  老年代每次回收少量的对象,因此采用Mark-Compact算法。

  • 属于方法区的永久代(Permanet Generation),它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  • 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老年代。
  • 当新生代的Eden Space和From space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
  • 如果To Space无法足够存储某个对象,则将这个对象存储到老年代。
  • 在进行GC后,使用的便是Eden Space和To Space了,如此反复。
  • 当对象在Survivor区躲过一次GC后,其年龄会+1。默认情况下年龄达到15的对象会被一到老年代中。

Java中四种引用类型

强引用

软引用

弱引用

虚引用

五、GC分代收集算法 VS 分区收集算法

1.分代收集算法

  当前主流的JVM垃圾收集都采用“分代收集”(Generation Collectiong)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适合的GC算法。

1) 新生代-复制算法

  每次垃圾收集都能发现大批对象已死,只有少量存货

2) 老年代-标记复制算法

  因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或“标记-整理”算法来进行回收,不必进行内存复制,且直接腾出空闲内存。

2.分区收集算法

  分区算法则将整个堆空间划分为连续的不同小空间,每个小空间独立使用,独立回收。这样做的好处可以一次回收多个小空间,根据目标停顿时间,每次合理的回收若干小区间(而不是整个堆),从而减少一次GC所产生的停顿。

六、GC垃圾收集器

  Java堆内存被划分为新生代和老年代,新生代主要使用复制和标记-清除垃圾回收算法;老年代主要使用标记整理垃圾回收算法,因此Java虚拟机中针对新生代和老年代分别提供了多种不同的垃圾收集器。
在这里插入图片描述
图源:垃圾收集器

由上图我们可以总结出几个结论:

  • 新生代垃圾收集器:Serial、ParNew、Parallel Scavenge;
    老年代垃圾收集器:Serial Old(MSC)、Parallel Old、CMS;
    整堆垃圾收集器:G1
  • 垃圾收集器之间的连线表示可以搭配使用,有如下几种组合:
    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  • 串行收集器:Serial:Serial、Serial Old
    并行收集器 :Parallel:Parallel Scavenge、Parallel Old
    并发收集器:CMS、G1

Serial垃圾收集器(单线程、复制算法)

  是最基本垃圾收集器,使用复制算法。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾回收结束。
  垃圾收集器虽然在垃圾收集过程中需要暂停所有其他的工作线程,但是他简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾回收效率,因此,Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器

ParNew垃圾收集器(Serial+多线程)

  ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器过程中同样也要暂停所有其他的工作线程。
  ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
  ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器

Parallel Scavenge收集器

  Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,前面介绍的垃圾收集器关注点是尽可能缩小垃圾收集时的用户线程停顿时间。而Parallel Scanvenge收集器是为了达到一个可控制的吞吐量。它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU时间,尽快完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

  • 可以用下面两个参数进行精确控制:
      -XX:MaxGCPauseMills 设置最大垃圾收集停顿时间
      -XX:GCTimeRatio 设置吞吐量大小

Serial Old收集器

  Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也是主要运行在Client默认的java虚拟机默认的老年代垃圾回收器。
在Server模式下,主要有两个用途:

  • 在JDK1.5之前的版本中与新生代的Parallel Scavenge收集器搭配使用;
  • 作为老年代中使用CMS收集器的后备收集方案。
  • 新生代Serial与老年代Serial Old搭配垃圾收集过程图:

Parallel Old收集器

  Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  1.作用于老年代
  2.多线程
  3.使用标记-整理算法
  除了具有以上几个特点,比较关键的是能和新生代收集器 Parallel Scavenge 配置使用,获得吞吐量最大化的效果。

CMS收集器

  CMS,全称为 Concurrent Mark Sweep ,顾名思义并发的,采用标记-清除算法。另外也将这个收集器称为并发低延迟收集器(Concurrent Low Pause Collector)
  这是一款跨时代的垃圾收集器,真正做到了垃圾收集线程与用户线程(基本上)同时工作。和 Serial 收集器的 Stop The World(妈妈打扫房间的时候,你不能再将垃圾丢到地上) 相比,真正做到了妈妈一边打扫房间,你一边丢垃圾。
  1.作用于老年代
  2.多线程
  3.使用标记-清除算法
  整个算法过程分为如下 4 步:
  一、初始标记(CMS initial mark):只是仅仅标记GC Root 能够直接关联的对象,速度很快,但是需要“Stop The World”  
  二、并发标记(CMS concurrent mark):进行GC Root Tracing的过程,简单来说就是遍历Initial Marking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
  三、重新标记(CMS Remark):修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。这个时间一般比初始标记长,但是远比并发标记时间短。
  四、并发清除(CMS concurrent sweep):对上一步标记的对象进行清除操作。
  由于整个过程最耗时的操作是第二(并发标记)、四步(并发清除),而这两步垃圾收集器线程是可以和用户线程一起工作的。所以整体来说,CMS垃圾收集和用户线程是一起并发的执行的。
  缺点:
  ①、对CPU资源敏感
  因为在并发阶段,会占用一部分CPU资源,从而导致应用程序变慢,总吞吐量会降低。
  ②、产生浮动垃圾
  由于CMS并发清理阶段用户线程还在工作,这个时候产生的垃圾,CMS无法在本次收集中处理掉它们,只能留在下一次GC时再将其处理掉,这部分垃圾称为“浮动垃圾”。
  ③、产生内存垃圾碎片
  因为采用的算法是标记-清除,很明显,会有空间碎片产生。

G1收集器

  这是当前收集器技术发展的最前沿的成果。可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,首发于JDK8中,是JDK9默认的垃圾回收器。
  这是因为它并不像前面介绍的所有垃圾收集器是区分新生代,老年代的,它作用于全区域将整个Java堆划分为多个大小固定的独立区域(Regin),并且跟踪这些区域的垃圾堆积面积,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收垃圾最多的区域,这样保证了G1收集器在有限的时间内可以获得最高的收集效率。
  它与前面讲的 CMS 垃圾收集器相比,有两个显著的改进
  ①、采用 标记-整理 的回收算法
  这样不会产生空间碎片
  ②、可以精确的控制停顿时间
  能让使用者明确指定一个长度为M毫秒的时间片内,消耗在垃圾回收上的时间不超过 N 毫秒。
  ③、作用于整个Java堆
  G1收集器不区分年轻代和老年代,是整堆垃圾收集器。

猜你喜欢

转载自blog.csdn.net/qq_43010602/article/details/112239317