三、JVM垃圾回收机制

3.1 堆为什么要分成年轻代和老年代?

在这里插入图片描述
因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;

年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;

老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法 ;

所以需要分成两个区域来放不同的对象;

1、绝大多数对象都是朝生夕灭的;

如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;

2、熬过越多次垃圾收集的对象就越难以回收;

如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ,同时也有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;

Minor GC/Young GC :新生代收集

Major GC/Old GC:老年代收集

Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;

Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:

(1)复制算法

(2)标记-清除算法

(3)标记-整理算法

3.2 JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区会怎么样?

此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;

解释:

会比较频繁的出发Full GC,开销太大

2、如果只有1个Survivor区会怎么样?【导致内存碎片】

刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;

如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;

如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;

在这里插入图片描述

解释:

如果只有一个Survivor区域,触发Minor GC后会有内存碎片问题的产生(内存不连续),大大降低了效率

所以得有第二个Survivor区域来装要存活的对象,从而使得第一个Survivor空闲、Eden也空闲,保证了内存空间的连续性——需要2个Survivor区域的原因解析

3.3 Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

一个eden区 ,新生代对象出生的地方;

两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;

统计和经验表明,90%的对象朝生夕死存活时间极短 ,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;

解释

8:1:1原因在于每次gc会有90%的对象被回收 ,这是个统计的结果

3.4 请介绍下JVM中的垃圾回收算法?

在这里插入图片描述

3.4.1 标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为‘ 标记 ’和‘ 清除 ’两个阶段;

1、标记

标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;

2、清除

标记后,对所有被标记的对象进行回收;

该算法如下图所示:

会发现回收后,内存空间就不连续了,有内存碎片
在这里插入图片描述
优点:

基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;

缺点:

1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

3.4.2 复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

在这里插入图片描述
优点:

实现简单,效率高,解决了标记-清除算法导致的内存碎片问题

缺点:

1、代价太大,将可分配内存缩小了一半,空间浪费太多了;

2、对象存活率较高时就要进行较多的复制操作,效率将会降低;

3.4.3 标记-整理算法

标记-整理算法是根据老年代的特点而产生的;

1、标记

标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;

2、整理

和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存;

标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为“Stop The World”即STW;

但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理

另外像cms垃圾收集器,平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;

优点:

1、不会像复制算法那样划分两个区域,提高了空间利用率;

2、不会产生不连续的内存碎片;

缺点:

效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

如下图所示:
在这里插入图片描述

3.4.4 分代收集算法

现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;

根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;

新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;——只需要复制少于10%的对象,效率很高!

老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

3.5 请介绍一下JVM垃圾收集器?

在这里插入图片描述
如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;

新生代收集器:Serial、ParNew、Parallel Scavenge [ˈpærəlel] [ˈskævɪndʒ]

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

垃圾收集器的最前沿成果:ZGC(Jdk11中引入了) 和 Shenandoah(Open JDK12,Oracle没引入,很前沿 )

解释:

目前在生产环境中,G1是比较先进的垃圾收集器了

3.5.1 Serial收集器[新生代、单线程]

新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作,所以有卡顿现象,效率不高,致使java语言的开发团队一直在改进垃圾收集器的算法和实现,但Serial收集器简单,不会有线程切换的开销,是Client模式下默认的垃圾收集器,-client, -server;

1
2
3
参数: -XX:+UseSerialGC

java -XX:+PrintFlagsFinal -version 打印jvm默认的参数值;

垃圾收集时间线如下:

所有线程卡住(停下来),进行垃圾收集,收集完毕,然后再继续…
在这里插入图片描述

3.5.2 ParNew收集器[新生代、多线程]

它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial;

Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,

1
2
3
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads=2":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

在这里插入图片描述

3.5.3 Parallel Scavenge收集器【新生代、多线程】

在这里插入图片描述
简称Parallel,它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%,有时候我们也把该垃圾收集器叫吞吐量垃圾收集器或者是吞吐量优先的垃圾收集器;而且这个垃圾收集器是jvm默认的新生代的垃圾收集器;

它提供一个参数设置吞吐量:

1
2
3
-XX:MaxGCPauseMillis 该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;

-XX:+UseAdaptiveSizePolicy 自适应新生代大小策略,默认这个参数是开启的,当这个参数被开启之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics);

如果我们不知道怎么对jvm调优,我们可以使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(最大停顿时间)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成,自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性;

1
2
3
4
5
6
7
8
9
10
11
12
13
参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器

java -XX:+PrintCommandLineFlags -version 打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

-XX:+

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

Parallel Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;

GC日志

这里常用一下JVM参数打印JVM的log:

1
2
3
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/dev/gc.log