JVM之GC

  • Post author:
  • Post category:其他


学习GC,个人认为可以从以下几方面入手:

1. 什么是GC
2. 什么是垃圾
3. 如何判断垃圾
4. 垃圾回收算法
5. Java堆中的分代思想
6. 垃圾回收器的种类
7. 三色标记算法
8. 垃圾回收方式



一、什么是GC

GC,指的是JVM中的垃圾回收机制。当对象成为垃圾时,就会被JVM中的垃圾回收机制所回收,释放对象所占用的空间,防止产生OOM(Out Of Memory)。



引言:根据上述GC的定义,引出一个名词:垃圾。在Java中,什么是垃圾?



二、什么是垃圾

这里说的垃圾实际上就是对象。那么满足什么条件的对象会被定义为垃圾?

在Java中,如果一个对象不可能再被引用时,这个对象就是垃圾,应该被GC所回收。



引言:已经知道在Java中,垃圾实际上就是指不再被引用的对象,那么应该如何判断一个对象是否是垃圾?



三、如何判断垃圾

Java中,判断垃圾的方式有两种,分别是:引用计数法、可达性分析。



1、引用计数法

给对象添加一个计数器,每当对象被引用时,则计数器加一;当引用失效时,计数器减一。当计数器的值为0时,表示该对象无任何引用,在GC时可进行回收。


注意:引用计数法虽然简单,但是存在一个致命问题——循环引用。


循环引用:

对象A、B相互引用,但是没有其他对象引用对象A、B。此时对象A、B应该是垃圾,但是由于他们相互引用,引用计数器的值不为0,因此GC无法对其进行回收。


注意:

引用计数法存在循环引用的问题,因此不被推荐使用。现在的JVM判断垃圾的方式是可达性分析。




2、可达性分析

从一系列”GC Root”对象出发,所有可达的对象,都是存活的对象;所有不可达的对象,都是需要被回收的垃圾。


上述中说到的


“GC Root”


是一个判断对象是否存在引用的集合。这个集合通常包括:

1. 虚拟机栈中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的属性
4. 本地方法栈中Native中引用的对象


引言:判断出垃圾之后,就需要对垃圾进行回收,那么怎么回收垃圾呢?



四、垃圾回收算法

JVM中的垃圾回收算法有三种,分别是:标记清除算法、标记复制算法、标记整理算法。



1、标记清除算法

标记清除算法可以分为两个阶段:标记和清除。在标记阶段,标记所有”GC Root”可达的对象。在清除阶段,将没有被标记到的对象进行回收。


标记清除算法的优缺点:

优点:不浪费空间内存,也不需要移动存活的对象;适合用于存活对象多的位置。

缺点:GC过后,容易产生空间碎片。因为垃圾对象并不是连续存放的,因此在GC过后可能会产生空间碎片。


用途:

标记清除算法常用于老年代,因为老年代中的对象生命周期较长。




2、标记复制算法

标记复制算法会将内存空间分为两块,每次只会使用一块。在GC时,可以分为两个阶段:标记和清除,标记所有”GC Root”可达的对象,在清除阶段,会将被标记的对象赋值到另一块未使用的内存中,然后清除正在使用的这块内存,之后两块内存扮演的角色(存储、待复制)进行转换。


标记复制算法的优缺点:

优点:使用标记复制算法,不产生空间碎片;适合用于需要移动对象少的位置。

缺点:因为内存空间被分为两块,每次只使用一块,会造成空间浪费。


用途:

标记复制算法常用于新生代,因为新生代中的对象生命周期较短。




3、标记整理算法

标记整理算法是标记清除算法的优化版,在进行垃圾清除之后,会将存活的对象整理到一起,减少了空间碎片的产生。


标记整理算法的优缺点:

优点:减少了空间碎片的产生;适合用于存活对象多的位置。

缺点:需要移动存活的对象。


用途:

标记整理算法常用于老年代,因为老年代中的对象生命周期较长。



引言:在描述垃圾回收算法的时候,提到了新生代、老年代。什么是新生代?什么是老年代?



五、Java堆中的分代思想

在JVM中,类加载时,会为创建的对象分配内存空间,一般情况下分配在Java堆中。因此Java堆主要用来存放对象。创建了对象,在对象成为垃圾的时候,就需要进行清除。那么应该使用标记清除算法?还是标记复制算法?还是标记整理算法?

如果在Java堆中,单纯使用这三种算法中的某一种算法,最终的垃圾回收效率都不会太好。因此,Java堆中使用的是分代算法。首先将Java堆中的内存空间分为新生代和老年代,再根据不同的区域,使用不同的算法,以达到高效率的垃圾回收。



1、新生代

Java堆的内存空间被分为两块,一块为新生代。大部分新创建的对象都在新生代中存放,根据IBM公司研究表明,这些新创建的对象有98%是朝生夕死的,存活的对象少,可以使用标记复制算法来对新生代的垃圾进行回收。

不同于简单的标记复制算法,新生代将空间划分为三个区域,Eden、From Survivor 0、

To Survivor 1。它们的存储空间比例为8:1:1。这么分配是因为有90%以上的对象存活时间短,当回收垃圾时,Eden和Survivor中存活的对象会复制到另一块Survivor中,再清除掉之前存储的Eden、Survivor内存。这么做,新生代中的内存空间利用率达到了90%,只有10%的内存空间浪费。如果像简单的标记复制算法一样,将内存对半分开,则会浪费一半的内存空间。



2、老年代

Java堆的另一块内存就是老年代。它用于存放JVM认为生命周期较长的对象(经历了多次Minor GC仍然存活的对象),以及需要大量连续存储空间的对象(大对象)。与新生代分区存储不同,老年代中只有一块大内存,称为Old Memory。因为老年代中存放的大部分是生命周期较长的对象,这些对象大部分在经历GC之后还可以存活下来,因此老年代使用的多是标记清除算法或标记整理算法。



引言:上述中一直提到垃圾回收算法,那么使用这些算法进行具体执行的是什么?也就是怎么使用这些算法进行垃圾回收?



六、垃圾回收器的种类

Java虚拟机的垃圾回收器有七种,分别是:

Serial



Serial Old



ParNew、Parallel



Scavenge



Parallel Old



CMS



G1


1、Serial:

串行收集器,使用单线程进行垃圾回收的回收器,因为是单线程回收,因此每次在进行垃圾回收时都是独占性的,其他任务都需要暂停,即Stop The World现象,等待垃圾回收完成。Serial是JVM中最古老的一种垃圾回收器,也是最基本的,适合用于单核并发能力弱的计算机。使用的是标记复制算法,作用在新生代上。


2、Serial Old:

与Serial回收器一样,是单线程独占性的垃圾回收器,属于Serial老年代版,因此也会出现Stop The World。Serial Old使用的是标记整理算法,作用在老年代上。


3、ParNew:

ParNew回收器,与Serial回收器一样,只是简单的将Serial的单线程变为多线程,它的回收策略、算法与Serial回收器一样,都是使用标记复制算法,作用在新生代上。因为ParNew是多线程的垃圾回收,因此在多核并发能力强的计算机上,进行垃圾回收时产生的Stop The World时间会比较短。但是在单核并发能力弱的计算机上,因为多线程之间进行状态切换的原因,性能上不如Serial回收器。


4、Parallel Scavenge:

与ParNew回收器一样,属于并行回收器,指的是垃圾回收线程是并行进行的,但依旧是独占式,会造成Stop The World。Parallel Scavenge可以通过

-XX:+UseAdaptiveSizePolicy

参数,打开自适应GC调节策略,在自适应模式下,新生代的大小、Eden和Survivor的比例、晋升老年代的GC年龄

灯参数,都会进行一个自适应调节,已达到堆大小、吞吐量、STW时间(Stop The World)的平衡点。


5、Parallel old:

是Parallel Scavenge回收器的老年代版,使用的是标记整理算法,其策略、参数与Parallel Scavenge收集器一样。


6、CMS:

CMS回收器,其注重系统停顿时间,是获取最短停顿时间为目标的回收器。它使用的是标记清除算法,配合三色标记算法,作用于老年代。

CMS回收器的缺点有:

1. 因为使用的是标记清除算法,所以容易产生空间碎片。
2. 并发标记阶段容易产生浮动垃圾,当此无法清除,下一次GC的时候才能清除。
3. CMS回收器具有并发和并行,对CPU资源敏感。

CMS回收器的执行过程:初始标记 –> 并发标记 –> 重新标记 –> 并发清除


7、G1:

G1回收器,它与前面描述的六种回收器都不一样,G1回收器把Java堆内存分割为很多不相关的区域(Region,物理上不连续)。可以通过

-XX:G1HeapRegionSize

参数设置Region区的大小,只能为2的幂次方。如果

-XX:G1HeapRegionSize

为默认值,即把设置的最小堆内存按照2048份均分,最终得到一个合理的大小。使用不同的Region可以用来表示Eden、Survivor、老年代等。G1回收器还增加了一个新的内存区域,叫做

Humongous内存区域

,主要用于存储大对象,如果对象的大小超过1.5个Region,就存放到Humongous内存区域中,如果Humongous区域放不下大对象,G1则会寻找连续的Humongous区域来存储,如果找不到连续的Humongous区域,则不得不启动Full GC,来获取合适的区域。G1每次根据允许的收集时间,优先回收价值最大的Region。在算法上,G1使用的是标记整理算法和标记复制算法,配合三色标记算法。

G1回收器的优点:

1. 分代收集:G1回收器可以回收新生代和老年代的垃圾,与之前单一区域回收的回收器不同。
2. 分区思想:G1回收器把Java堆内存分割成多个Region,尽管Java堆被分割成多个Region,但依旧存在新生
代和老年代,新生代依旧有Eden、Survivor 0、Survivor 1的区分,只是这些区域不需要是连续的,也不用
固定大小和数量。这样更有利于垃圾回收和空间分配。
3. 空间整合:通过Region区域和标记整理算法的配合,使得Java堆中的空间碎片更少,Region和标记整理算
法都有减少空间碎片产生的功能。这种配合有利于程序的长时间运行,不会因为分配大对象内存空间不足而导
致下一次GC的提前。
4. 可预测停顿时间模型:G1通过设置`-XX:G1HeapRegionSize`参数,可以指定长度为M毫秒的时间片段内,
消耗在GC的时间不能超过N秒。
5. 将垃圾回收效益最大化:G1每次根据允许的GC时间,优先回收价值最大的Region,从而达到回收效益最大
化的特点。

G1回收器的执行过程:初始标记 –> 并发标记 –> 重新标记 –> 并发清除



引言:在描述CMS和G1回收器的时候有提到,二者都配合使用三色标记算法,什么是三色标记算法?



七、三色标记算法

标记清除算法或者标记整理算法,分为标记和清除阶段,在清除阶段,需要扫描整个引用链的不可达对象,然后将垃圾对象清除。这个过程中,必须进行Stop The World。GC过程中,所有的任务都必须停止,不能做任何事情,这对用户体验来说,并不友好。在CMS回收器之前,都是使用这种标记–>清除的方式,因此STW时间较长。

从CMS回收器开始,与G1回收器都是配合使用三色标记算法,三色标记算法是指将对象划分为三种颜色,分别是黑色、灰色和白色。

黑色代表从”GC Root”开始,已经扫描过它全部引用的对象。

灰色代表从”GC Root”开始,扫描过对象本身,但是还没扫描它的全部引用对象。

白色代表还没扫描过的对象。

在清除阶段,如果对象依旧为白色,则证明该对象由”GC Root”开始。不可达,是垃圾,需要进行回收。

仅仅将对象划分为三种颜色,这并不能解决上述长时间的STW问题。在CMS和G1回收器中,垃圾回收的流程由

标记-->清除

变为

初始标记-->并发标记-->重新标记-->并发清除

四个步骤。

1. 初始标记:将"GC Root"直接引用的节点标记为灰色,这个阶段需要STW。
2. 并发标记:通过初始标记中的灰色节点,遍历整个引用链,将可达的节点标记为黑色,这个阶段不需要STW。
3. 重新标记:这个阶段是针对并发标记阶段的多标、漏标进行校正,这个阶段需要STW。
4. 并发清除:将白色节点进行清除,这个阶段不需要STW。

通过两段的流程与四段的流程进行对比可以看出,四段的流程通过并发操作,将耗时最多的遍历引用链与其他用户线程并发执行,这么做可以减少STW的时间。但是将标记节点与用户线程进行并发执行,容易因为对象引用的问题带来多标和漏标的问题。


多标:

多标是指原本应该回收的对象,被错误的标记为黑色,导致垃圾回收时无法进行回收。多标问题的出现,是在并发标记阶段,当对象被标记为存活对象之后,其引用关系被删除(如将对象置为null),从可达对象变为不可达对象。多标问题的出现会导致产生浮动垃圾,不过在下次GC时,浮动垃圾会被回收,只要浮动垃圾所占用的内存不多,基本上不会带来太大的问题。


漏标:

漏标是指原本应该存活的对象,被错误的遗漏标记,从而导致在进行垃圾回收时被当成垃圾给回收了。漏标的问题同样是因为在并发标记阶段,遍历完对象以后,对象引用了一个灰色对象的引用对象,而灰色对象又断开了这个引用。导致在遍历灰色对象的全部引用时,没有遍历到这个对象,从而这个对象呈白色,最终被回收。漏标会导致程序功能出现问题。


CMS和G1如何解决多标和漏标的问题:

在通过上述分析得知,多标带来的问题并不会太严重,只是会保留垃圾对象,但是在下一次垃圾回收时,就会进行回收,即便保留下来的垃圾对象所占据的内存太多导致Java堆中没有多余的内存可以创建新对象,也会触发Minor GC、Major GC、Full GC,从而清理掉这些保留下来的垃圾对象。而漏标所带来的问题就比多标严重,它可能会导致程序出现问题,影响程序的运行。

通过上面的描述可以得知,漏标的产生是存在两个条件的:

1. 至少有一个对象在划分为黑色之后引用了白色对象。
2. 灰色对象在扫描完自己全部引用之前,断开了与白色对象之间的引用。

只有当

同时满足

上述两个条件时,才会出现漏标的情况。因此要避免漏标的出现,只需要破坏其中一个条件即可。根据上述两个条件,产生了两种破坏方式:1. 增量更新;2. 原始快照。在CMS回收器中使用的是增量更新的方式,而G1回收器使用的是原始快照的方式。

在了解增量更新、原始快照之前,需要先了解读屏障和写平展这两个概念。读写屏障的诞生,也是为了避免漏标的发生。

写屏障

是指在给某个对象的成员变量赋值前后,加入一些处理(类似spring中的AOP);

读屏障

是指在读取成员变量之前,先记录下来。


G1的原始快照

针对的是漏标产生条件的第二个,当灰色对象断开了引用的白色对象之前,将白色对象记录下来,在重新标记阶段,根据白色对象再进行扫描标记。这样就可以避免漏标的问题。但是,这也会带来新的问题,就是这个白色对象,有可能是真的被取消引用了,再次将其标记,就会变成多标,不过多标是可以接受的,在下一次GC的时候就会对多标的对象进行回收。


CMS的增量更新

针对的是漏标条件产生的第一个,当一个黑色对象引用白色对象之后,将这个引用记录下来,在重新标记阶段,以这个黑色对象为根,进行遍历扫描,被黑色引用的白色对象,就会成为存活状态。这种方式的缺点就是重新扫描这部分记录的黑色对象,需要耗费时间。

三色标记算法的出现,有效的减少了垃圾回收时所产生的STW时间,尽管在并发标记阶段,容易产生多标和漏标的问题,但是这些问题在CMS和G1中被解决了,总的来说,三色标记算法的使用,提高了垃圾回收的

效益



引言:在知道了什么是垃圾,如何判断垃圾,清除垃圾使用的算法、回收器之后,那么垃圾回收的方式有哪些呢?



八、垃圾回收方式

垃圾回收的方式有三种,Minor GC(Young GC)、Major GC(Old GC)、Full GC。



1、Minor GC(Young GC)

Minor GC发生在新生代,触发的条件是当新生代中Eden区域存储满了之后,会触发Minor GC,将Eden和一块Survivor区域存活的对象复制到另一块Survivor中,存活对象的

GC年龄+1

,然后清除正在存储的Eden区、Survivor区,之后两个Survivor区的存储、待复制角色进行互换。当下一次Eden区满了之后,继续重复执行此类操作。默认情况下,GC年龄达到15之后,就会晋升到老年代中,因此,Minor GC之后,通常老年代的内存占用量会有所升高。



2、Major GC

Major GC发生在老年代,Major GC是由Minor GC触发,当Minor GC进行时,如果新生代中的内存空间不够,则会触发老年代的Major GC,清除老年代空间,将一部分新生代对象晋升到老年代中,为新生代腾出空间。



3、Full GC

Full GC发生在整个Java堆中,清除新生代+老年代的内存空间。Full GC可以称为是Minor GC和Major GC的结合。当准备触发Minor GC时,如果发生老年代中剩余的内存空间比以往晋升的空间小,则不会触发Minor GC,转而触发Full GC。因为Full GC清除的内存空间包含老年代,因此其STW时间是Minor GC的十倍以上。在Java程序中,要尽量避免Full GC的发生。


注意:

在JDK1.8之前,即1.7到以前版本,存在永久代,当永久代的内存空间满了时,也会触发Full GC。


注意:

Minor GC、Major GC、Full GC只是一个俗称,不同的垃圾回收器中,对应不同的回收方式!!!



参考资料

微信公众号:树哥聊编程《JVM系列》



版权声明:本文为W_Y_Long原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。