【JVM】GC垃圾回收


一:GC原理

GC(Garbage Collection:垃圾回收)基本原理:将内存中不在使用的对象进行回收,GC中用户回收的方法称之为回收器,由于GC需要消耗一定的资源和时间的,GC主要作用于堆空间,根据对象的生命周期的特征进行分析按照新生代,老年代的方式来对对象进行收集,尽可能少的缩短GC操作对应用程序的暂停

  • 对新生代的对象进行收集称之为Minor GC
  • 对老年的对象的收集称之为Full GC
  • 程序中主动调用System.gc()强制执行GC是Full GC

不同的对象引用类型,GC采用的不同的方法进行回收,JVM对象引用分为四种类型

  • 强引用:默认情况下,对象采用的是强引用,在对象没有其他引用时,GC才会回收,当对象有至少一个引用时,JVM即使抛出异常也都不会回收该对象
  • 软引用:软引用所作用于的对象,在内存不足时才会被回收,一般适用于缓存场景
  • 弱引用:弱引用作用的对象,只要发生GC就一定会被回收
  • 虚引用:虚引用只是用来得知对象是否被GC

二:对象被标记为垃圾的方法

1、引用计数法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

  • 缺点:无法检测循环引用的问题
public class Demo{
  public static void main(String[] args){
    GCobject  o1 = new GCobject();//1
    GCobject  o2 = new GCobject();//1
    
    o1.instance = o2;//2
    o2.instance = o1;//2
    //下面我们想销毁o1和o2所指对象,虽然令其为空,但前面的操作让对象计数器为2,下面让计数器-1,计数器还剩1;那也就是说对象不会被垃圾回收
    o1 = null;//1
    o2 = null;//1
    }
}
class GCobject{
    public Object instance = null;
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述

2、可达性分析

可达性分析是将程序中所有的引用关系看成一张图,从一个GC Roots开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点,当所有的引用节点被查找完毕之后,剩余的节点认为是没有用的节点,无用的节点则被发任务是可回收的对象

可作为GC Roots的对象包含以下几种:

  • 虚拟机栈中的引用对象(栈帧中的局部变量表)
  • 方法区的静态变量和常量的引用对象
  • 本地方法栈中JNI引用的对象

在这里插入图片描述

三:垃圾回收算法

1、标记-清除算法

这种算法分为两部份:标记、清除两个阶段, 标记阶段是从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态,清除阶段在扫描完成之后将没有标记的对象给清除掉。

  • 标记阶段:标记的剁成使用可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都做上一个标记,一般在对象的header中,将其标记为可达对象。
  • 清除阶段:对堆内存进行整个遍历,如果发现某个对象没有被标记为可达的对象(通过读取header中的信息),则将其进行回收。
    在这里插入图片描述
    标记-清除算法缺点
  • 空间问题
    标记清除会产生大量的不连续的内存碎片,内存碎片化太多空间可能会导致后续的程序在运行过程中分配大对象时,无法找到足够的连续空间而不得不触发一次垃圾回收过程
  • 效率问题
    在标记和清除阶段都要遍历整个堆空间,在堆空间中对象数量特别大时,对堆空间的遍历无疑是和消耗时间的,而且GC过程会导致应用程序停顿。

2、复制算法

复制算法将堆空间按照容量大小划分成大小相同的两块,每次只使用其中一块,当一块使用内存完了,就将还存活的对象复制到另一块内存中,然后将这一块内存中所有的对象一次性清除掉
在这里插入图片描述
复制算法简单高效,并且优化了标记清除算法的效率低,内存碎片化多的问题

存在缺点

  • 将内存缩小为原来的一半,浪费了一半的内存空间,代价太高
  • 如果对象存活率比较高,极端一点假设对象100%存活,就需要将所有的对象复制一遍,消耗的时间代价也比较大

3、标记-整理算法

标记-整理算法和标记-清除算法很像,标记-整理的算法在标记的过程和标记-清除算法的标记阶段一样,后续步骤整理阶段不是直接对可回收的对象进行回收,而是让所有存活的对象向一端移动,然后直接清理掉另一端的内存
在这里插入图片描述
标记-整理算法弥补标记-清除阶段的内存碎片化的问题,避免了复制算法内存减半的代价

标记-整理算法缺点:

  • 效率不高:不仅要标记存活对象,还要管理所有的存活对象的引用地址,在效率上不如复制算法

4、分代回收算法

分代回收算法思想按照对象存活的生命周期不同将内存划分为不同的几块,一般划分为新生代、老年代(永久代)

特点:

  • 新生代:朝生夕灭,存活的时间很短,采用复制算法来回收
  • 老年代:经过多次Minor GC而存活下的对象,存活周期长,标记清除算法或者标记整理算法
    (采用的回收算法基于回收器来决定的)

新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集。

老年代中对象存活率比较高,不适用复制算法,如果采用复制算法它没有额外的空间进行分配担保,因此必须使用标记清除或者标记整理算法进行回收。

在这里插入图片描述

新生代中的对象几乎都是“朝生夕灭”的(达到98%),现在的商业虚拟机都采用复制算法来回收新生代。由于新生代的存活率低,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的From Survivor空间、To Survivor空间,三者的比例为8:1:1.每次使用Eden和From Survivor区域,To Survivor作为保留空间。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的。GC进行时,Eden区中所有存活对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1)的对象会被移到老年代中,没有达到阈值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代存活的对象都在To Survivor区。接着From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区。总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。此时复制算法就不合适了。

分代回收:

我们从一个object1来说明其在分代垃圾回收算法中的回收轨迹。

  • 1、object1新建,出生于新生代的Eden区域。
    在这里插入图片描述
  • 2、Minor GC,object1还存活,移动到From Survivor空间,此时还在新生代。在这里插入图片描述
  • 3、Minor GC,object1仍然存活,此时会通过复制算法,将object1移动到To Survivor区域,此时object1的年龄age+1。在这里插入图片描述
    *4、Minor GC,object1仍然存活,此时survivor中和object1同龄的对象并没有达到survivor的一半,所以此时通过复制算法,将From Survivor和To Survivor区域进行互换,存活的对象被移动到了To Survivor。在这里插入图片描述
  • 5、Minor GC,object1仍然存活,此时survivor中和object1同龄的对象已经达到survivor的一半以上(To Survivor区域已经满了),object1被移动到了老年代区域。在这里插入图片描述
  • 6、object1存活一段时间后,发现此时object1不可达GCROOTS,而且此时老年代空间比率已经超过了阈值,触发了Maior GC(也可以认为是Full GC,但具体需要垃圾收集器来联系),此时object1被回收了。Full GC会触发Stop The World。在这里插入图片描述
    在以上的新生代中,我们有提到对象的age,对象存活于survivor状态下,不会立即晋升为老年代对象,以避免给老年代造成过大的影响,它们必须要满足以下条件才可以晋升:
  • 1、Minor GC之后,存活于survivor区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。
  • 2、动态对象,如果survivor空间中相同年龄的所有的对象大小的综合和大于survivor空间的一半时,年纪大于或等于该年纪的对象就可以直接进入老年代。

四:内存的分配和回收

分配策略:

  • 对象优先分配在Eden区。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。
  • 动态对象的年龄判定(年龄超过阈值或survivor空间相同年龄所有对象大小总和大于survivor区的一半,年龄大于或等于该年龄的对象直接进入老年代)。

堆分了Eden、两个Survivor、Tenured共四个区域,Eden与Survivor的大小比是8:1,Eden和Survivor称为新生代,Tenured称为老年代(JDK8已经没有了持久代)。
 
当新对象产生时,存放在Eden区,当Eden区存放不下时会触发Minor GC,将Eden中存活的对象复制到一个Survivor区中,然后继续存放对象到Eden区,当Eden区放不下时触发Minor GC,将Eden区和非空闲的Survivor区中存活的对象复制到空闲的Survivor区中,往复操作。
 
每经过一次Minor GC,对象的年龄加1,当对象的年龄达到阈值(默认15)进入Tenured区。如果在Minor GC期间发现存活的对象无法进入空闲的Survivor区,则会通过空间分配担保机制使对象提前进入Tenured区。如果在Survivor空间中的相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到指定的阈值。

内存分配担保机制:

在执行Minor GC前,JVM会首先检查Tenured区是否有足够的空间存放新生代尚存活的对象,由于新生代使用复制收集算法,为了提升内存利用率,只使用了其中一个Survivor区作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor区无法容纳的对象直接进入老年代,但前提是老年代需要有足够的空间容纳这些存活对象。但存活对象的大小在实际完成GC前是无法明确知道的,因此Minor GC前,JVM会首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小,如果条件成立,则进行Minor GC,否则进行Full GC(让老年代腾出更多的空间)。然而取历次晋升的对象的平均大小也是有一定风险的,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure,老年代也无法存放这些对象了),此时就只好在失败后重新发起一次Full GC(让老年代腾出更多的空间)。

五:垃圾收集器

我们知道,JVM堆内存分为新生代和老年代,新生代采用复制算法,老年代采用标记-清除或者标记-整理算法来收集和清理垃圾,关于算法的具体实现便是接下来要了解的垃圾收集器。

收集器的发展历程:Serial收集器 → Parallel收集器 → CMS收集器(Concurrent Mark Sweep) → G1收集器(Garbage First)。

在进行垃圾回收时,会暂停所有的工作线程,直到垃圾回收完成,垃圾收集器的不断迭代就是为了优化减少停顿的时间。

在这里插入图片描述
使用垃圾收集器,可以设置垃圾收集器的相关参数:

  • -XX:+UseSerialGC,虚拟机运行在client模式下的默认值,Serial+Serial Old。
  • -XX:+UseParNewGC,ParNew+Serisl Old,在JDK1.8被废弃,在JDK1.7还可以使用。
  • -XX:UseConcMarkSweepGC,ParNew+CMS+Serial Old。
  • -XX:+UseG1GC,G1+G1。

1、Serial收集器

Serial收集器是单一线程收集器,运行在client端,是在JDK1.3.1之前唯一的垃圾收集器。

优势:

  • 简单高效,对于单个CPU的环境,Serial收集器由于没有线程交互的开销,专心做垃圾回收,因此可以获得最高的单线程收集效率。
    在这里插入图片描述

Serial收集器是最原始的一款垃圾收集器,也称之为串行收集器,顾名思义,它是单线程运行的,而且不止如此,它在收集垃圾的时候,会暂停其他所有的工作线程,直到收集结束,被称之“Stop The World”。想象一下,比如你在看电影,每看五分钟需要暂停几秒钟,这显然是令人难以接受的。

2、ParNew收集器

ParNew收集器是Serial收集器的多线程版本,运行在Server端。

特点:

多线程进行垃圾回收、其余行为(控制参数、收集算法、Stop The World、对象分配规则、回收策略)与Serial收集器完全一致,随着CPU的数量增加,对于GC时系统资源的有效利用还是很有好处的,默认开启的收集线程数与CPU的数量相同。

控制参数:

  • -XX:ParallelGCThreads 参数:限制垃圾收集的线程。

优势:

除了Serial收集器,只有它能与CMS收集器配合使用。

在这里插入图片描述

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程收集垃圾之外,其他行为基本和Serial收集器的实现完全一样。ParNew收集器只有在多核CPU的环境下才能发挥出它的优势(多线程收集速度快,停顿时间缩短),如果是单核CPU它甚至不如Serial收集器的效果好(单核CPU的线程切换导致额外开销)。

3、Parallel Scavenge收集器

Parallel Scavenge与ParNew类似,也是一款并行多线程收集器,相比于ParNew,它的特点则是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),如果虚拟机总共运行了100分钟,垃圾收集花了1分钟,那么吞吐量变为100-1/100=99%。

停顿时间越短越适合与用户进行交互的程序,良好的响应速度可以提升用户体验,而高吞吐量则是可以高效利用cpu,主要用于在后台运算不需要进行用户交互的任务。

Parallel Scavenge提供了两个参数来控制吞吐量:

  • -XX:MaxGCPauseMillis (控制停顿时间(jvm尽量不超过设置的时间),单位ms)
  • -XX:GCTimeRatio (吞吐量大小,大于0小于100)

但你千万不要以为把停顿时间的参数设小,吞吐量参数设大就可以让垃圾收集的速度变快,停顿时间的缩短是靠牺牲吞吐量和新生代空间来换取的:系统把新生代调小,比如由1000兆调节为700兆,收集700兆的空间速度必然比1000兆快,但是相应的收集频率会增高,原来10s收集一次,每次停顿100ms,现在需要5s收集一次,每次停顿70ms(相当于10s停顿140ms),停顿时间确实下降了,但是吞吐量也降了下来。

所以,Parallel Scavenge也被称为“吞吐量优先收集器”,此收集器还有一个参数:-XX:+UseAdaptiveSizePolicy,打开这个参数之后,不需要我们再额外设置新生代的大小以及新生代eden和survivor的比例等参数了,jvm会根据当前系统的运行情况动态调整这些参数以提供最合适的停顿时间和吞吐量,这种调节方式称为GC自适应的调节策略,同时这也是Parallel Scavenge和ParNew的重要区别之一。

4、Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法,运行在client端。

在这里插入图片描述

另外还可以在Server模式下:JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用,可以作为CMS的后备方案,在CMS发生Concurrent Mode Failure时使用。

5、Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,是一个多线程收集器,使用标记-整理算法,JDK1.6中开始提供使用。

在这里插入图片描述

Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。

6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。

因为它基于标记-清除算法,并发收集、低停顿,运行过程复杂,共分为四步:

  • 初始标记:
    仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”。
  • 并发标记:
    就是进行追踪引用链的过程,可以和用户线程并发执行。
  • 重新标记:
    修正并发标记阶段因用户线程继续运行导致标记发生变化的那部份对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”。
  • 并发清除:
    清除标记为可以回收的对象,可以和用户线程并发执行。

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程同时进行,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

在这里插入图片描述

缺点:

  • 对CPU资源敏感。事实上,面向并发设计的程序对CPU资源都较为敏感,在并发阶段,他虽然不会使用户线程停顿,但是也会因为占用了一部分CPU资源而使应用程序变慢,总吞吐量就会降低。
  • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”导致另一次Full GC(收集老生代成为Full GC)。由于CMS在并发清理阶段用户线程依然运行着并不断产生垃圾,这部分垃圾出现在重新标记之后,所以在本次GC中无法清理,这部分垃圾就称为浮动垃圾。CMS在垃圾收集的时候用户线程仍在运行,所以他不能向其他收集器一样等到老生代几乎填满再进行回收,需要预留一部分空间供并发时的程序使用,可以通过:-XX:CMSInitIatingOccupancyFaction的参数值来调节触发收集的百分比,一般不需要特意动它。如果预留空间无法满足程序运行的需要,那么就会出现Concurrent Mode Failure,这个时候就轮到Serial Old收集器登场了,JVM会临时使用Serial Old来重新对老年代进行垃圾收集,这同时也就意味着系统停顿时间变长,所以此参数设置过高容易引起大量Concurrent Mode Failure,反而降低性能!
  • 产生大量内存碎片。CMS利用的是标记-清除算法来进行垃圾收集(比标记-整理快),这必然会不可避免的产生内存碎片,内存碎片过多时,就算剩余空间很足,但是无法找到连续的内存空间去分配新来的大对象,就会不得不提前触发Full GC。我们可以通过开启:-XX:UseCMSCompactAtFullCollection参数来解决此问题(默认开启),这样CMS在顶不住要进行Full GC时会对内存碎片进行合并整理,但这也会使得停顿时间变长(内存整理无法并发执行)。通过-XX:CMSFullGCsBeforeCompaction可以设置执行多少次不合并整理的Full GC后,执行一次带合并整理的Full GC,默认为0,即每次进入Full GC时都会进行碎片整理。

7、G1收集器

收集器发展最前沿的成果之一,可以运行与服务端和客户端。

特征:

  • 并行与并发:
    能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。
  • 分代收集:
    G1收集器可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生代和已经存活一段时间的对象。
  • 空间整合:
    整体上看采用标记-整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。
  • 可预测的停顿:
    除了追求低停顿外,还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过M毫秒,这点优于CMS收集器。

为什么能做到可预测的停顿?
 
是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1收集器将内存分为大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。G1跟踪各个Region区域获得其收集价值大小【注:《深入理解JAVA虚拟机》中这样解释:回收所获得的空间大小以及回收所需要的时间的经验值】,在后台维护一个优先列表;每次根据允许的收集时间,优先回收价值最大的Region区域(名称Garbage-First的由来);这就保证了在有限的时间内可以获取尽可能高的收集效率。

回收过程步骤(与CMS收集器较为相似):

  • 初始标记:
    仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top At Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region区域中创建对象,需要“Stop The World”。
  • 并发标记:
    从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行。
  • 最终标记:
    修正并发标记阶段因用户线程继续运行导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”。
  • 筛选回收:
    首先对各个Region区域的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region区域中的垃圾对象。回收时采用复制算法,从一个或多个Region区域复制存活对象到堆上的另一个空的Region区域,并且在次=此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。
    在这里插入图片描述
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是几个 JVM 垃圾回收相关的面试题及其答案: 1. 什么是垃圾回收垃圾回收是指在程序运行过程中,自动回收不再使用的内存空间,以提高内存利用率和程序的性能。 2. JVM垃圾回收机制是什么? JVM垃圾回收机制采用分代收集算法,将内存分为新生代和老年代,分别采用不同的垃圾回收算法来回收内存。 3. 新生代垃圾回收器有哪些? 新生代垃圾回收器主要有 Serial、ParNew、Parallel Scavenge 等。 4. 老年代垃圾回收器有哪些? 老年代垃圾回收器主要有 Serial Old、Parallel Old、CMS、G1 等。 5. 什么是对象的引用计数算法? 引用计数算法是一种简单的垃圾回收算法,它通过记录每个对象被引用的次数,当引用次数为 0 时,即可将该对象回收。 6. 引用计数算法的缺点是什么? 引用计数算法的缺点是无法处理循环引用的情况,如果两个对象之间相互引用,它们的引用计数会一直不为 0,导致无法回收。 7. 什么是标记-清除算法? 标记-清除算法是一种常见的垃圾回收算法,它将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,标记所有活跃对象,将其打上标记;在清除阶段,清除所有未标记的对象。 8. 标记-清除算法的缺点是什么? 标记-清除算法的缺点是会产生大量的内存碎片,会导致内存利用率降低。 以上是一些常见的 JVM 垃圾回收面试题及其答案,希望能对你有所帮助。在面试过程中,需要根据具体的问题进行回答,同时也需要对垃圾回收机制和算法有清晰的认识,才能更好地回答相关的问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值