深入浅出JVM(三)垃圾回收


前言

通过前俩篇博客,相信对jvm已经初步的认识,介绍了“食物从口入喉”以及“五脏六腑”,接下来就要介绍食物吃进去后,哪些被人体吸收了哪些当成垃圾丢弃了。专业术语来讲,就是哪些对象存活了哪些对象被当成垃圾回收了。
那么值得思考的问题有以下三个

如何判定对象为垃圾对象?

判定为垃圾对象后如何回收?

本篇博客将围绕这俩个问题,来介绍GC的一些算法,GC的一些收集器以及一些扩展的知识点。


提示:以下是本篇文章正文内容,下面案例可供参考

一、如何判定对象为垃圾对象?

1、引用计数法

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0 的对象就是不可能再被使用的。
上图分析
在这里插入图片描述
图中可知,当无外界引用对象时,对象却互相引用。这样可以说是死循环,如果JVM用引用计数法,那么至始至终都无法回收这类情况的对象。虽然该方法比较灵活,效率也较高,但是因为这样的缺陷,JVM无法拿他作为判断对象是否存活的方法。

2、可达性分析算法

前面讲到引用计数算法,有内存泄漏的发生。而可达性分析算法能很好的解决这一缺点。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
这是官方的解释,大白话翻译过来就是,有一系列对象作为节点的起点,顺着这个起点去搜索引用的对象链,如果一个对象没有任何链相连接,那么该对象不可用。

在这里插入图片描述
如图所示,红色的对象则是不可用的对象。这就好比葡萄,你拎起的时候很多脱落的,就是要被淘汰的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。譬如Java类的引用类型静态变量。
方法区中常量引用的对象。譬如字符串常量池(String Table)里的引用。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
需要注意的是,就算可达性分析算法不可达的对象,也并不是非死不可的。他们只是暂时处于缓刑阶段,要判他死,至少经历俩次标记过程。

二、垃圾收集算法

目前在JVM中比较常见的三种垃圾收集算法分别是,标记清除算法、复制算法、标记整理算法。

1、标记清除算法

标记清除算法,就是当内存被耗尽的时候,就是停止整个程序。意思是有名的stop the world,停顿的时候整个应用程序线程都会被暂停,没有任何响应。并不是重新启动,只是卡了而已,stw完了以后,所有的应用程序线程会在完成GC之后恢复。
标记:
另外需要注意的是,很多博客以及书上甚至周志明的《深入理解Java虚拟机》上关于这个知识点的标记说的是标记需要回收的对象。但是实际上是标记是引用的对象,是可达性分析算法能可达的对象。需要被回收的垃圾,根本都不可达,拿什么去标记呢?
清除:
收集器对内存从头到尾进行线性的遍历,如果发现某个对象在GC Roots中不可达,也就是上一步标记尚未标记,则回收。
这是宋红康老师说的,虽然周志明老师也很值得敬佩。但是在这个争论上我还是觉得宋老师讲的更具有说服力,合情合理。
在这里插入图片描述
效率不高、而且需要STW影响用户体验,并且清理的空闲内存不是连续的,有内存碎片。

2、复制算法

复制算法就是将内存分为俩个大小相等的容量,每次只用一块,这块用完了,就将这块活着的对象复制到另一块空内存区,再把已使用的这块内存空间清空。每次都是对整个半区进行内存回收,另一块用来放这一快的存活对象,这样就不会有内存碎片。关于这个算法在我上一篇博客中关于新生代中from区和to区有详细介绍。
相比标记清除算法,主要优点就是不会有内存碎片问题。
但是如果一个区的存活对象太大,那么这个复制算法效率就会低很多。

3、标记整理算法(标记压缩算法)

前面提到的俩种算法,可以说各有千秋也各有瑕疵,标记清除有内存碎片,复制算法虽然解决了内存碎片问题,但是一旦存活对象过多,每次大量复制必定造成效率低下。像老年代,大部分都是存活对象,你用复制算法的话,岂不是每次复制一大堆。标记清除也可以,只是会有内存碎片。
这个时候标记压缩算法诞生了
在这里插入图片描述
可看到,不仅仅清理了垃圾,还将内存进行了整理。因此标记整理算法也可以称为标记清除整理算法。二者最大的区别就是存活的对象被移动了。
这种算法,虽然消除了复制算法内存减半的高额代价,也消除了内存碎片的问题。但是效率时是要低于复制算法的,因为他是前俩者算法的综合体,比复制多了个标记,比标记清除多了个移动,而且移动对象的内存地址也是有风险的,移动过程中需要STW。

那么说来说去,这些算法各有这么多缺点,那玩个球?目前几乎所有GC都是采用分代收集算法执行垃圾回收。分代收集算法它并不是一种算法,也是一种操作。它把不同的算法的优缺点进行考虑然后放置合适的区域进行垃圾收集。

比如,年轻代,对象存活率低,回收频繁,那么用复制算法是最好的。
老年代,区域大,生命周期长,存活对象多,肯定不能用复制算法了,一般用标记清除算法或者标记整理算法。

三、垃圾收集器

前面说的各种收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。关于垃圾收集器比较杂,并没有明确规定那个好那个不好,不同的厂商(oracle-jdk,open-jdk)不同的版本(jdk版本)虚拟机所提供的垃圾收集器都可能会有很大的差别。

垃圾收集器很多,不过垃圾算法也就是前面介绍的几种。垃圾收集器一般都是搭配使用,总的来说可以划分为俩个区域。新生代和老年代。
在这里插入图片描述
从最初的单线程Serial收集器到最新的G1收集器,是一种演变过程。但并不是一种淘汰过程,不是说Serial最差或者说G1最好。放在合适的场景才是最好的,一般都是俩种搭配使用,G1属于例外。
目前Jdk1.7和Jdk1.8 我们默认使用的都是Parallel Scavenge(新生代)+Parallel Old(老年代),到了Jdk1.9默认使用的G1垃圾收集器。这里重点介绍CMS以及G1收集器,其他不做深究。

1、五种简单的垃圾收集器

Serial收集器:历时最悠久的垃圾收集器,单线程,新生代工作,复制算法。通常与Serial Old搭配使用。

Serial Old收集器: 单线程,老年代工作,标记压缩算法。

ParNew 收集器:这个收集器相当于多线程的Serial收集器,其他性质几乎没有区别,多线程,新生代工作,复制算法。

Parallel Scavenge Pae收集器:和ParNew很像,也是并行的多线程收集器,也在新生代工作,并且也是采用复制算法。那么和ParNew不同之点正是他存在的原因。它是一个很看重吞吐量的收集器。吞吐量=运行代码时间/(运行代码时间+垃圾收集器时间),因为他与吞吐量关系密切,所以也被称作为“吞吐量优先”收集器。另外还有个重要的特点,他有自适应调节策略没这是ParNew收集器所没有的。所谓自适应调节策略,根据当前运行的一个情况。动态监控性能,动态调整内存的分配,已达到更高的性能。不能与cms搭配使用,因为他们的基础框架不同。详情

Parallel old收集器:这个和Parallel Scavenge 收集器几乎是一样的,只是工作区域和工作算法不同。parallel old 收集器在老年代工作,算法是标记压缩法。

通过介绍上面五种收集器,很容易发现,除了ParNew外,都是处CP的,很容易理解记忆。

新生代 复制算法,Serial ParNew 以及Parallel Scavenge
老年代 标记压缩算法 Serial Old以及Parallel old
而Parallel 这对情侣注重的是吞吐量以及有自适应调节策略。

CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

那么接下里介绍的一款说是具有划时代意义的垃圾收集器,叫做CMS(Concurrent Mark Sweep)这是需要重点介绍的,因为它和后面G1有一定的相似性。

2、CMS

老年代的收集器,有多线程还有高吞吐量的收集器了,但这个CMS硬是在老年代收集器作为新秀,强有力的站了出来。它是一种以获取最短回收停顿时间为目标的收集器。也是第一款真正意义上的并发收集器。这一特点,特别适用于互联网站以及BS系统的服务端上,反应快,用户体验感好。

它的基于标记清除算法实现的,值得注意的是,前面俩种老年代收集器都是标记压缩算法哦。

具体步骤分为四步

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除
    在这里插入图片描述

建议仔细看一遍标记清除算法,有助于更好理解CMS的四大步骤。
初始标记就是标记GC Roots能直接关联的对象,“葡萄串的首根枝,没有标记其他分支上的葡萄”。STW,所有线程都停止,只有初始标记线程在走。stw

并发标记就是标记GC Roots能间接关联到的对象,“标记分支上的葡萄”。耗时较长。
未STW,并发和其他用户线程一起。非stw

重新标记就是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。“有些初始标记本来不在根枝上,并发标记后又长根枝上了,有些之前在根枝上,并发标记后又脱落了” STW,所有线程都停止,只有重新标记线程在走。stw

并发清除就是清除删除掉标记阶段判断以及死亡的对象,释放内存空间,耗时较长。也是并发的,可以和用户其他线程一起运行。非stw

由上面可以很明显的看到,耗时的俩个阶段,一个是并发标记以及并发清理。但是这俩个阶段都是并发的,都不需要暂停用户线程,相当于后台进行。STW仅仅只是初始标记以及重新标记,而这俩个阶段很快。所以总体来说CMS最大的优点就是大大缩短了STW

但事物都是有俩面性的,他的优点恰恰是他的缺点,当我们并行标记和并行清除的时候,其他用户线程也在执行,这就对我们的内存有很大的要求,又要能进行GC相关的线程,又要能进行用户线程。如果用户线程的可用内存不够,就会提前触发Full GC。而且这样对CPU的负载也大。

CMS收集器用的标记清除法,所以有内存碎片也是很正常的。

另外还有个缺点,就是浮动垃圾无法清除,在上面的步骤中,我们一边清理垃圾,一边有用户线程运行,这就意味着,一边清理垃圾,一边会生成垃圾,而这些垃圾是我们当前没有标记的,只有等第二次GC的时候才能清除,这种垃圾就叫做浮动垃圾。

因为有浮动垃圾,所以‐XX:CMSInitiatingOccupancyFraction=75 ‐XX:+UseCMSInitiatingOccupancyOnly这个值设置的比较小。当老年代对象超过75%就开始fullgc

3、G1

从jdk9后(包括jdk9),垃圾收集器默认为G1,Oracle官方说这是功能最全的的垃圾收集器,主要从以下四点对它进行解析。
并发与并行
关于并发与并行的概念,很容易搞混。并发,实际上就是多个线程切换执行,但切换的时间极其短暂,看上去就是多个线程同时发生的一样。任意一个时间段,有且只有一个线程在执行,这就是并发。而并行,就是真正的多个线程同时执行。当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。G1就是并行且并发的垃圾收集器。
并行体现:可以多个GC线程同时工作,利用多核计算器能力,此时用户线程STW。
并发体现:GC线程可以和用户线程交替执行,不用暂停用户线程,非STW。

分代收集
前面介绍的各种收集器,要么负责新生代,要么负责老年代。而G1雨露均沾,即负责新生代,也负责老年代。它将堆空间分为若干个区域,这些区域就包括了年轻代和老年代。值得注意的是,它并不是按8:1:1这样黄金比例分隔新生代,而是若干个大小相等的独立区域,各个区物理上不连续。而从逻辑上新生代和老年代都是连续的。

空间整合
因为G1是分区的垃圾收集器,那么不在像前面几种垃圾收集器一样,固定只采取某种单一的算法。这里是以区为单位,区之间是复制算法,但是整个可以看作是标记压缩算法,都可以避免内存碎片。正因为每次选取的部分区域进行GC,这样也就缩小了回收的范围,因此STW也能得到好的控制。

可预测的停顿时间模型
这个功能很牛逼,它能预测每个区域回收的时间。在使用者指定的时间片刻内,优先回收价值比较大的区域。比如这个区域回收后内存空间最大,而且回收时间最短,那么这个区域就是价值最大的。关于价值的衡量,后台会有一个表。价值从大到小排序,优先执行价值大的区域。这就尽可能的提高收集效率。

G1回收具体过程

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

其实这个过程和CMS回收的过程前三步基本一样的,只是CMS第三步叫重新标记,而G1第三步叫最终标记,原理是一样的。
而第四步就不一样了,G1的筛选回收,首先对每个区域的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这也体现了G1的名称,Garbage first,垃圾优先。

4、各种垃圾收集器的总结

从一开始单核单线程Serial,到并行的ParNew,再到并发的CMS,再发展到并发并行的G1,大致总结一个表单。
在这里插入图片描述
值得重复的是,并不是最新的垃圾收集器最好,也不是最先出来的垃圾收集器最差,根据不同场景使用不同的垃圾收集器,这样才是最好的。

总结

以上介绍了如何判断对象可回收,以及回收的算法,还有回收器的特点。也是回答了一开始的俩个问题,但这都是理论知识。
关于JVM调优才是实践,实践离不开理论的支持,所以前三篇知识很重要,而理论如果离开了实践便是纸兵器。所以关于JVM调优,是系列博客的最终章也是压轴章,会在后面跟进博客。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值