深入理解JVM底层实现_3 JVM垃圾回收算法和垃圾回收器

                                       深入理解JVM底层实现

                                       3 JVM垃圾回收算法和垃圾回收器

                                                                                                                                                                                                田超凡

                                                                                                                                                                                        2019-11-09

1 JVM垃圾回收机制

JVM垃圾回收机制,简称GC(Garbarge Collection),主要用来解决JVM运行时堆区已占用且长期不用的对象内存空间的释放问题,防止堆满和堆溢出,并且GC垃圾回收机制会在程序运行时根据指定的不同的GC垃圾回收算法和堆区新生代、老年代各类GC垃圾回收器来实现动态垃圾回收,通过配置一系列JVM-GC参数可以实现不同的GC垃圾回收策略和使用不同的GC垃圾回收器。在一次JVM运行期间,具体JVM的堆区是什么时候真正实现内存回收的,具体回收的对象大小都是无法人为干预的,但是我们可以通过GC垃圾回收日志来查看GC垃圾回收执行情况,GC垃圾回收日志原则上只是提供一种参考,方便我们快速跟踪定位JVM运行周期中GC垃圾回收的执行过程。

System.gc()方法在调用时会告诉JVM需要显示开始一次GC垃圾回收了,但是具体JVM是否在调用本方法时执行了GC垃圾回收、具体执行了多少次GC垃圾回收、回收了多少内存资源都是无法人为精准控制和定位的,因为JVM操作堆区对象(包括内存分配、管理、释放、垃圾回收等)都是调用C语言native函数实现的,直接根据服务器不同操作系统内存大小进行动态操作,在不同内存、不同配置、不同型号的操作系统中JVM执行效率和执行结果都会有部分差异,人工是无法强制决定和干预的,但是可以通过GC垃圾回收机制尽可能提高程序运行效率,进一步实现JVM调优,GC垃圾回收机制是JVM性能调优的基础。

JVM堆区进行GC垃圾回收的实现原则是,对于堆区中没有引用的对象、引用了但是没在引用链上(没有互相引用的对象)是需要定时进行GC回收的,至于定时的频率和GC垃圾回收具体执行的时间片都是调用native方法实现,无法人为干预。

 

2 JVM垃圾回收算法

常用的几种GC垃圾回收算法和实现策略:

 

2.1 引用计数算法

1.概述:引用计数算法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在(对象互相引用的情况),因此引用计数算法只存在于早期JDK的GC垃圾回收设计方案,后来随着对象互相引用的情况非常容易出现,所以主流的Java虚拟机里面都没有选用引用计数算法来管理内存,只是把引用计数算法当成一种GC垃圾回收算法的基础实现思想。 
2.实现策略:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。引用计数算法在主流的JVM中不常用的最主要原因是它很难解决对象之间相互循环引用的问题。

 

2.2 根搜索算法(可达性分析算法)

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达,GC Roots和对象之间没有直接连接)时,则证明此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

(4). 本地方法栈中JNI(Native方法)引用的对象。

下面给出一个GCRoots的例子,如下图,为GC Roots的引用链。

 

 

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

从上图,reference1、reference2、reference3都是GC Roots,可以看出: 
reference1-> 对象实例1; 
reference2-> 对象实例2; 
reference3-> 对象实例4; 
reference3-> 对象实例4 -> 对象实例6; 
可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。 
而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

 

2.3 标记清除算法

标记清除算法有两个阶段。

1. 标记阶段:找到堆中所有可访问的存活对象,做个标记

2. 清除阶段:遍历堆,把未被标记的对象回收

标注清除算法一般应用于老年代,因为老年代的对象生命周期比较长。

标记清除算法的优点和缺点如下: 

1. 优点

- 是可以解决循环引用的问题

- 必要时才回收(内存不足时)

2. 缺点:

- 回收时,应用需要挂起,也就是stop the world(STW),所有线程暂停。

- 标记和清除的效率不高,尤其是要扫描的对象比较多的时候

- 会造成过多内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)

 

2.4 复制算法

1.概述:如果JVM使用了复制算法,一开始就会将堆区幸存者(survivor)中的可用内存分为两块,from域(S0域)和to域(S1域),每次只是使用S0域,S1域则空闲着。当S0域内存不够了,开始执行GC操作,这个时候,会把S0域存活的对象拷贝到S1域,然后直接把S0域进行内存清理。

复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法将对象在多个区之间(Eden To Survivor S0,Survivor S0 To Survivor S1)进行多次拷贝时效率比较高。

JVM内存划分为新生代与老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区),然后在Eden –>Survivor Space 以及S0 Survivor Space 与S1 Survivor Space 之间实行复制算法。 

JVM在应用复制算法时,并不是把新生代中的内存按照1:1来划分的,这样太浪费内存空间了。一般的JVM给新生代分配的内存大小占比都满足Eden:Survivor=8:2。也即是说,Eden区:Survivor S0区:Survivor S1=8:1:1。

这样就可以保证在新生代中,始终有90%的空间是可以用来创建对象的,而剩下的10%用来存放GC垃圾回收后仍然存活的对象。

 

  1. 实现策略:
  1. .当Eden区满的时候,会触发第一次young gc把还活着的对象从Eden区拷贝到Survivor S0区;当Eden区第二次触发young gc的时候,会扫描Eden区和Survivor S0区域对两个区域进行GC垃圾回收,经过这次GC垃圾回收后还存活的对象,则直接复制到Survivor S1区域,并将Eden和Survivor S0区域清空。

(2).当后续Eden又发生young gc的时候,会对Eden和Survivor S1区域进行垃圾回收,存活的对象复制到Survivor S0区域,并将Eden和Survivor S1区域清空。

(3).可见部分对象会在Survivor S0Survivor S1区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

复制算法的特点:

优点在存活对象不多的情况下,性能高,能解决内存碎片和标记清除算法中导致的引用更新问题。

缺点会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。

注意: 万一存活对象数量比较多,那么S1 Survivor域的内存可能不够存放,这个时候会借助老年代的空间。(老年代分配空间担保)

 

3.复制算法实现GC垃圾回收事件触发描述

(1).新生代Eden伊甸园区GC回收,拷贝到Survivor幸存者区:younger gc

(2).新生代Survivor幸存者区GC回收,拷贝到Older老年代:minor gc

(3).老年代GC回收,回收整个堆内存(因为老年代在堆中已经没有额外空间进行分配担保,所以老年代如果装满了就需要整个堆区重新回收:full gc

 

2.5 标记压缩算法

1.概述:标记压缩算法和标记清除算法非常相同,但是标记压缩算法在标记清除算法之上主要用来解决内存碎片化主要作用在老年代进行GC垃圾回收。

 

2.标记压缩算法排序策略:

任意顺序 : 不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动对象;

线性顺序 : 考虑对象的引用关系,例如a对象引用了b对象,则尽可能将a和b移动到一块;

滑动顺序 : 按照对象原来在堆中的顺序滑动到堆的一端。

标记压缩算法特点:

优点:解决内存碎片问题

缺点在标记压缩阶段,由于移动了可用对象,需要去更新对象引用。

 

2.6 分代算法

1.概述:分代算法根据对象的存活周期的不同将内存划分成几块:新生代(Eden+Survivor S0+Survivor S1)和老年代(Older),这样就可以根据各个年代的特点采用最适当的垃圾收集算法,可以用抓重点的思路来理解这个算法。

新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免多次扫描导致的开销。

新生代(复制算法)

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

老年代(标记清除算法+标记压缩算法)

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须遵循“标记-清除-压缩”算法进行回收。

新创建的对象默认优先被分配在新生代Eden,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。

老年代区存放Young区Survivor满后触发minor GC(新生代Survivor幸存者区满)后仍然存活的对象 ,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老,也直接存放到Old区中。如果Old区满了,将会触发Full GC回收整个堆内存。

 

3 JVM垃圾回收器

新生代垃圾回收器采用复制算法实现GC垃圾回收

老年代垃圾回收器采用标记清除算法+标记压缩算法实现GC垃圾回收

新生代垃圾回收器:Serial 串行垃圾回收、ParNew 并行垃圾回收、Parallel-Scavenge 吞吐量优先

老年代垃圾回收器:CMS 并行标记清除、SerialOld 串行垃圾回收、Parallel-Older 吞吐量优先

注意:老年代CMS垃圾回收器只能和新生代Serial、ParNew垃圾收集器结合使用,不能和Parallel-Scavenge垃圾回收器结合使用

 

3.1 新生代垃圾回收器:基于复制算法实现GC垃圾回收

Serial串行收集器(单线程收集器,触发Stop The World暂停除垃圾回收线程外的所有运行的线程)

这是发展最悠久的垃圾收集器,在jdk1.3的时候只能用我们serial垃圾回收器。

他是一个单线程的垃圾回收器,用在我们的新生代复制算法,在桌面应用比较多(单线程服务器上,堆内存比较小的应用使用效率比较高)

 

当新生代的复制算法在进行gc回收时会暂停除垃圾回收线程之外的所有的线程,这个步骤简称STW (Stop The World)

 

ParNew 并行收集器(多线程收集器)

在清理时候采用ParNew采用多线程收集也是在新生代复制算法

 

Parallel Scavenge吞吐量优先收集器(多线程收集器)

他也是采用我们的复制算法,多线程实现垃圾回收,达到可以控制的吞吐量,计算公式是:用户代码运行时间/(用户代码运行时间+GC暂停时间)

-XX:MaxGCPauseMillis 垃圾回收器最大停顿时间

-XX:GCTimeRatio 吞吐量大小  (0,100) 默认最大99

 

CMS并行标记清除收集器(Concurrent Mark Sweep)

主要基于标记清除算法+标记压缩算法实现

工作过程: 

1.初始标记:使用可达性分析法(根搜索算法)标记gc roots直接关联上的对象。

2.并发标记:由前阶段标记过的对象出发,所有沿引用链可到达的对象都在本阶段中标记。

3.并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World(暂停所有线程)

4.重新标记:标记那些因为用户程序继续运作产生的新生代跨代引用的垃圾对象

5.并发清理:清理所有的垃圾对象。

6.并发重置:将原本的标记对象重置

 

优点:支持多线程并发垃圾收集、GC低停顿

缺点:占用大量的cpu资源,无法处理浮动垃圾,会产生碎片化

注意:老年代CMS并行标记清除垃圾处理器只能和新生代Serial或者ParNew结合使用,不能和Parallel-Scavenge结合使用

 

转载请注明原作者

 

发布了100 篇原创文章 · 获赞 10 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_30056341/article/details/103004222