JVM之堆的垃圾回收 -- 03

文章目录

      • 一、引用
        • 1.强引用
        • 2.软引用
        • 3.弱引用
        • 4.虚应用
      • 二、判断对象是否存活的算法
        • 1. 引用计数法
        • 2.可达性分析算法
      • 三、垃圾收集算法
        • 1.标记清除法(Mark-Sweep)
        • 2.复制算法(Copying)
        • 3.标记整理算法 (Mark-Compact)
        • 4.分代收集算法
      • 四、HotSpot的算法实现
        • 1.枚举根节点
        • 2.安全点
        • 3.安全区域
      • 五、垃圾收集器
        • 1.新生代垃圾收集器
          • 1.1 串行回收:serial收集器
          • 1.2 并行回收:ParNew收集器
          • 1.3 并行回收:Parallel Scavenge收集器
        • 2. 老年代垃圾收集器
          • 2.1 Serial Old 收集器
          • 2.2 Parallel old收集器
          • 2.3 CMS - Concurrent Mark Sweep收集器
        • 3. G1 收集器
        • 4.ZGC
          • 4.1 ZGC的回收过程
        • 5. 收集器总结

介绍:
  MinorGC :新生代的GC
  MajorGC :老年代的GC
  FullGC = MajorGC + MinorGC

一、引用

1.强引用

 例如 :Objec obj = new Object(); GC不可达的时候回收

2.软引用

 Java提供SoftReference 类 ,GC的时候如果发现内存不足,这个软引用会被干掉
例如使用方法: ConcurrentHashMap<> map = new ConcurrentHashMap>();做缓存,当然现在有成熟的方案做缓存如redis。

3.弱引用

  只要出现GC就会被回收,不管是否内存不足 例如WeakReference类 ThreadLocal中使用了WeakReference> ,这里需要注意内存泄露的问题,(这里还不太熟,后面研究下)

4.虚应用

  Java提供PhantomReference类,无法通过虚引用获取到一个对象实例;为一个对象设置虚引用唯一目的是为了能在这个对象被收集器回收的时候能够收到一个系统通知
多个垃圾回收器及算法的目的都是为了 减少FullGC

二、判断对象是否存活的算法

1. 引用计数法

  引用计数器的实现非常简单,对于一个对象A,,只要任何一个对象引用了A,则A的计数器就加1,当引用失效时,计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用。其实现也特别简单,只需要为每个对象配备一个整型计数器即可。

引用计数器有两个严重的问题:

  • 无法处理循环引用的情况,两个不可达对象之间的循环引用。
  • 在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作。对系统性能有一定的影响。
    JVM之堆的垃圾回收 -- 03_第1张图片

2.可达性分析算法

  通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。对于上面的相互引用的问题,就可以解决了。
如下:
JVM之堆的垃圾回收 -- 03_第2张图片
在Java语言中,可作为GC Root的对象包括以下几种对象:

  • Java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI本地方法的引用对象

三、垃圾收集算法

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

  标记清除算法将垃圾回收分成两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此没有被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。

标记清除算法的问题:

  • 第一效率低,标记和清除效率都低
  • 第二产生空间碎片,造成大量不连续的空间问题。
    JVM之堆的垃圾回收 -- 03_第3张图片

2.复制算法(Copying)

  为了解决效率问题,它将内存分为容量为大小相等的两块,每次只使用其中的一块,当这块内存快用完了,就将还存活这的对象复制到另外一窥按上面,然后把已经使用过的内存一次性清理,这样就不用关系内存碎片的问题。一般用于存活对象少、垃圾对象多的情况,一般是新生代;

问题: 内存缩小为原来的一半
JVM之堆的垃圾回收 -- 03_第4张图片

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

  标记整理算法是一种老年代的回算法。它在标记清除算法的基础上做了一些优化。和标记清除算法一样,标记整理算法也需要从根节点开始,对所有可达节点做一次标记,将所有存活对象压缩到内存的另外一端,并保持它们之间的引用关系。这样既避免了内存碎片,又不需要两块相同的内存空间。

 一般老年代的垃圾回收需要时间比较长是因为,老年代中存活对象比较多,而使用标记压缩算法需要将所有的存活对象压缩到内存的一端。
JVM之堆的垃圾回收 -- 03_第5张图片

4.分代收集算法

  当前的商业虚拟机的垃圾收集都采用这种算法,分代算法将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的算法,以提高垃圾回收的效率。

 一般来说新生代主要使用复制算法,老年代使用标记清除算法或者标记整理算法。

 新生代中将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时将还存活着的对象一次性复制到另外一块Survivor,最后清理掉Eden和刚才的Survivor。

 HotSpot默认Eden和Survivor比例是8:1;也就是新生代中可用内存为整个新生代容量的90%。只要10%被浪费,当然当Survivor不够的时候就需要依赖老年代进行分配担保。

四、HotSpot的算法实现

1.枚举根节点

  从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。
  另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。
  由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。 在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么烈性的数据计算,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

2.安全点

  在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。
  实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。

  对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。

  • 抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。
  • 主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。

3.安全区域

  解决安全点中有现成处于sleep或者blocked状态。

  使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。
  安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。

  当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

总结:
JVM之堆的垃圾回收 -- 03_第6张图片

五、垃圾收集器

  现在没有任何一种垃圾收集器是万能的,所以对于不同的应用选择最合适的收集器是最好的。

  横线上的收集器Young genneration是新生代,下面的Tenured generation 是年老代,连了线的表示可以组合使用;GI两边使用。下面细讲。
JVM之堆的垃圾回收 -- 03_第7张图片

1.新生代垃圾收集器

  新生代垃圾收集器都是复制算法来做的。

这里涉及到并行和并发问题:

  • 并行: 指的是多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态,如ParNew, Parallel Scavenge, Parallel Old收集器

  • 并发: 指用户线程和垃圾收集线程同时执行,不一定并行,也是有可能交替执行,如CMS, G1收集器。

1.1 串行回收:serial收集器

  Serial收集器作用域新生代中,采用复制算法、串行回收和 Stop-the-World机制的方式执行内存回收。工作图如下:
JVM之堆的垃圾回收 -- 03_第8张图片

  • 优点:如果是单个CPU的宿主环境中,使用serial收集器+serial old收集器的组合执行Client模式下的内存回收将会是不错的选择。基于串行回收的垃圾收集器适用于大多数对暂停时间要求不高的Client模式下的JVM,CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

  • 缺点:降低程序的吞吐量

1.2 并行回收:ParNew收集器

  ParNew收集器是serial收集器的多线程版本.除了使用了多线程进行垃圾手机外,其余包含Serial收集器可用的所有控制参数,收集算法,Stop the World、对象分配规则、回收策略等和Serial收集器完全一样。工作图如下:
JVM之堆的垃圾回收 -- 03_第9张图片
  ParNew收集器在新生代中采用并行回收、复制算法和stop-the-world机制,可以和CMS和Serial Old(作为老年代)联合使用。在CPU的环境中不会比Serial收集器有更好的效果,甚至存在线程交互的开销。甚至超线程技术也不一定能保证超过Serial收集器。默认开启的收集线程数和CPU的数量相同。

  当然对于CPU太多了的可以通过-XX:ParallelGCThreads参数限制垃圾收集的线程数。ParNew收集器也是使用-XX:UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:UseParNewGC选项强制指定它。

  • 优点:在多CPU的宿主环境下,可以充分利用多CPU、多核心等物理硬件资源优势、可以迅速的完成垃圾回收,提升程序的吞吐量。
1.3 并行回收:Parallel Scavenge收集器

  Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

目标:该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:

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

  停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是:

  • 控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,(不一定越小越好)
  • 以及直接设置吞吐量大小的-XX:GCTimeRatio参数(垃圾收集时间占总时间的比例)

Parallel Scavenge收集器还有一个参数:

  • -XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。虚拟机会根据当前系统运行情况动态调整参数用来提供合适的停顿时间或者吞吐量,这种调节方式称为GC自适应的调剂策略(GC Ergonomics)

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

2. 老年代垃圾收集器

  老年代的收集器中Serial Old,Parallel Old是标记-整理的收集算法,CMS(ConcurrentMarkSweep)使用的是标记-清除的垃圾收集算法。

2.1 Serial Old 收集器

  Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,主要两大用途:

  • 在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用

  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Serial Old收集器的工作工程图:
JVM之堆的垃圾回收 -- 03_第10张图片

2.2 Parallel old收集器

  Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。工作过程图如下:
JVM之堆的垃圾回收 -- 03_第11张图片

2.3 CMS - Concurrent Mark Sweep收集器

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

  CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

  • 1)初始标记阶段-----
    • 标记老年代中所有的GC Roots对象;速度快,会stop-the-world
    • 标记年轻代中活着的对象引用到的老年代的对象;
  • 2)并发标记阶段-----从“初始标记”阶段标记的对象开始找出所有存活的对象,这里和工作线程可以并发运行
  • 3)重新标记阶段-----修正并发标记期间用户程序继续运行产生变动的那一部分对象的标记记录,会stop-the-world,比初始标记时间长,比并发标记时间短。
  • 4)并发清除阶段:清理垃圾,这里有碎片产生。

  整个过程中耗时最长的并发标记和并发清除过程收集器线程可以和用户线程一起工作,所以总体来说,CMS收集器的内存回收过程是和用户线程一起并发执行的。

工作过程图如下:
JVM之堆的垃圾回收 -- 03_第12张图片

CMS收集器主要优点:并发收集,低的回收停顿时间。

CMS三个明显的缺点

  • 1)CMS收集器对CPU资源非常敏感。 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。

  • 2) CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

  • 3)大量空间碎片产生。 空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

CMS缺点总结:
在这里插入图片描述

3. G1 收集器

  使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。如下:
JVM之堆的垃圾回收 -- 03_第13张图片
G1收集器的特点:

  • 1)并行与并发,充分利用多CPU,多核的硬件环境
  • 2)分代收集
  • 3)空间整理 (标记整理算法,复制算法),不会产生内存空间碎片
  • 4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

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

G1 内存“化整为零”的思路

在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:
初始标记和并发标记和CMS相同

  • 1)初始标记
  • 2)并发标记
  • 3)最终标记,将并发标记变化的记录记录在线程Remembered Set Logs里面,最终标记阶段需要把这里变化的记录合并到Remembered Set 中。
  • 4)筛选回收 ,最后对各个Region的回收价值和成本进行排序。

工作过程图:
JVM之堆的垃圾回收 -- 03_第14张图片

通过JVM参数-XX:+UseG1GC使用G垃圾回收

4.ZGC

  ZGC的回收停顿时间非常的短,但是现在还不太稳定,未来也许就是趋势。

 ZGC将堆划分为Region作为清理,移动,以及并行GC线程工作分配的单位。分为有2MB,32MB,N× 2MB三种Size Groups,动态地创建和销毁Region,动态地决定Region的大小。

4.1 ZGC的回收过程
  1. Pause Mark Start - 初始停顿标记 (只标记GCRoot的直接引用,不用扫描GCRoot所有的链路)(只有这里会停顿,时间很短)
    JVM之堆的垃圾回收 -- 03_第15张图片
  2. Concurrent Mark -并发标记 (并发地递归标记其他对象 ,5和8也被标记为live
    JVM之堆的垃圾回收 -- 03_第16张图片
  3. Relocate - 移动对象
    对比发现3,6,7是过期对象,也就是中间的两个区域需要被压缩清理,所以需要把4,5,8对象往右移,移动过程中,有一个forward table记录这种转向。
    JVM之堆的垃圾回收 -- 03_第17张图片
  4. Remap - 修正指针;最后将指针更新指向新地址 。

JVM之堆的垃圾回收 -- 03_第18张图片

5. 收集器总结

Parallel Scavenge 由于是要保证吞吐量,所以在OOM的时候,CPU并不会占用太高

CMS 由于是减少回收停顿时间,所以在OOM的时候,垃圾回收器会不停的回收,不停的回收,所以CPU占用非常的高,影响用户的代码运行。

参考:《深入理解Java虚拟机》

你可能感兴趣的:(JVM学习笔记)