【JVM】(三)垃圾回收算法和垃圾收集器

3 篇文章 2 订阅

1.如何确定一个对象是垃圾

在上一篇《【JVM】(二)运行时数据区及内存模型》说到了,对象是在不断的生成并在堆中分配内存空间,但是堆内存空间是有限的,于是我们需要对堆中不再使用的对象进行回收,以便于回收后的空间可以分配更多的对象。这种不再使用的对象,就叫做垃圾

确认一个对象是不是垃圾,有两种方式。

  • 引用计数法
  • 可达性分析

1.1.引用计数法

简单的说,就是每个对象都有一个引用的计数器,每多一个地方引用它,计数器就+1,每失效一个引用,计数器就-1,计数器为0时就表示对象不再被使用,可以回收掉。
但是这个方式很难解决对象之间互相引用的问题,大家的计数器都不为0,都不能被回收,俗称“垃圾抱团”。在主流的Java虚拟机中没有选用引用计数法来管理内存。

1.2.可达性分析

通过部分可作为GC Roots的对象作为起始点,从起始点开始一个节点一个节点的往下搜索,形成一条条的引用链,当一个对象没有任何引用链到达GC Roots时,则证明此对象不可用。即使是抱团的垃圾也会被标记出来。
在这里插入图片描述

1.2.3.什么对象可以作为GC Roots

在回收标记的阶段中,可以被认为是一定存活的对象,就可以作为GC Roots,例如:

  • 虚拟机栈,栈帧本地变量表引用的对象。
  • 本地方法栈,JNI引用的对象。
  • 方法区中,静态变量或常量引用的对象。
  • Thread对象。
  • 类加载器。
  • ……

2.垃圾回收算法

JVM中常用的算法有三种:

  • 复制回收算法
  • 标记-清除算法
  • 标记-整理算法

在JVM中不是单一的使用某一种算法,而是不同的算法配合使用,共同完成垃圾回收的功能。

2.1.复制回收算法

这种算法的思路在于将内存划分为同样大小的两块,每次使用其中一块分配空间,另一块保留。在垃圾回收时,将分配空间的内存区域中的存活对象复制到保留区后,再清空当前分配空间的区域,然后将两块区域的角色互换一下。这种算法不用考虑内存碎片的问题,简单高效,但是每次只使用一半的内存空间,代价比较大。

回收的图示如下:
在这里插入图片描述

2.2.标记-清除算法

顾名思义,这个算法分为两个阶段,第一个阶段将垃圾标记出来,第二个阶段将标记的垃圾清除掉。两个阶段的效率都不高,这种算法容易出现大量的内存碎片,在分配大对象时可能会导致GC的更加频繁。
回收图示如下:
在这里插入图片描述

2.3.标记-整理算法

标记的过程和标记-清除算法一样,标记完成后将存活的对象都向一端移动,然后将端边界外的垃圾全部回收。
图示如下:

在这里插入图片描述

3.垃圾回收器

垃圾回收器实现了内存回收,HotSpot虚拟机的垃圾回收器如下图:
在这里插入图片描述
图中新生代和老年代回收器之间有连线的代表可以搭配使用。

3.1.几个基本概念

后面会提到并行和并发的收集器,有区别于在多线程部分的概念,垃圾收集器中的并行的并发分别指的是:

并行:多个GC线程并行工作,但此时用户线程暂停。
并发:GC线程和用户线程同时工作。

垃圾回收中的吞吐量,吞吐量越大,说明垃圾收集时间占用比例越小:

吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间)

3.2.Serial和Serial Old

Serial一款历史悠久的垃圾收集器,是一款单线程的垃圾收集器,采用的是复制回收算法。
Serial Old是Serial的老年代版本,同样是单线程收集器,采用标记-整理算法。
在垃圾回收的时候会发生STW(Stop The World),也就是说在垃圾回收的时候,会将所有的用户线程暂停。
单线程的收集器没有线程交互的开销,比较简单高效,对于内存空间不大的client端来说,也是一个不错的选择。

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

3.3.ParNew

ParNew是Serial的多线程版本,是一款并行收集器。
适用于新生代,采用复制回收算法。
是Server端的首选新生代垃圾收集器。

ParNew和Serial Old的运行图示如下:
在这里插入图片描述

3.4.Parallel Scavenge

一款新生代的垃圾收集器,和ParNew一样,也是并行收集器,属于是ParNew的升级版,简称PS。这款垃圾收集器关注的是吞吐量优先,可以通过下面的两个JVM指令参数来设置停顿时间和吞吐量。

-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间,可以配置一个大于0的毫秒数
-XX:GCTimeRatio 设置吞吐量大小,可以配置(0,99)之间的整数,默认99,GC时间占比为 1/(1+99),也就是1%。

停顿时间并不是配置的越短越好,越短的停顿带来的结果可能是回收频率的增加,之前10秒收集1次,停顿100毫秒,现在5秒收集一次,停顿70毫秒,虽然每次停顿的时间变短了,但是吞吐量也降低了。

除此之外,和ParNew还有一个重要的区别,Parallel Scavenge有一个自适应调节策略,开启后只需要配置堆的最大值-Xmx,以及上面的停顿时间和吞吐量配置,其它的新生代和老年代比例,新生代晋升到老年代的对象大小等都不需要再配置了。GC会根据当前系统运行情况的监控动态调整这些参数。
可以通过下面的参数配置:

-XX:+UseAdaptiveSizePolicy

3.5.Parallel Old

PS的的老年代版本,使用标记整理算法,简称PO,这款收集器在JDK1.6之后才开始提供,它出现后才缓解了PS在垃圾回收器的组合中只能选择Serial Old而无法到达最佳吞吐量优化的尴尬地位。到JDK1.9之前,PSPO都是一组默认的垃圾收集器。
它们的运行图示如下:
在这里插入图片描述

3.6.CMS

Concurrent Mark Sweep,顾名思义,它是一种并发的收集器,采用的是标记-清除算法,同时属于老年代的垃圾收集器。CMS实现了在用户线程和GC线程的并发运行,注重的是停顿时间优先,适用于互联网或B/S架构的服务端上。

CMS的垃圾回收分为4个步骤:

  • 初始标记:GCRoots直接关联的对象,这一步速度比较快。
  • 并发标记:GCRoots依次往下寻址的过程。
  • 重新标记:对并发标记期间变动的对象做标记。
  • 并发清理:清除标记处的垃圾。

初始标记和重新标记两个步骤依然会Stop The World,但理论上最耗时的阶段都可以与用户线程一起工作,执行图示如下:
在这里插入图片描述

优点:并发收集,减少停顿时间。
缺点:
①并发阶段占用了一部分用户线程资源。
②会产生内存碎片。
③无法处理标记阶段后出现的浮动垃圾,可能出现Concurrent Mode Failure导致另一次Full GC。

在CMS中可以开启内存碎片整理功能,并设置多少次GC后执行一次内存整理解决内存碎片的问题,但是停顿时间就会变长。
另外,针对浮动垃圾的产生,CMS中有一个-XX:CMSInitiatingOccupanceFraction参数可以指定老年代中的对象占用了多少后启动一次GC,JDK1.6中是92%,如果Concurrent Mode Failure出现的频繁,可以考虑降低这个启动阈值。因为Concurrent Mode Failure出现后,会启动Serial Old进行一次垃圾回收,这样停顿时间也会变长。

3.7.G1(Garbage-First)

3.7.1.G1的内存模型

G1是一款针对服务器的垃圾收集器,同时具有低停顿和高吞吐的特性,针对的是多核的CPU和大容量的内存,是JDK9的默认垃圾收集器。G1针对的是整个Java内存,也就是说同时适用于新生代和老年代,整体上使用标记-整理算法,局部使用复制回收算法
相对于其他垃圾收集器的经典内存模型,G1采用了新的内存模型,物理上不再隔离,只在逻辑上保留了分代的特性,内存模型如下:
在这里插入图片描述
G1将Java内存划分成如上图的一个一个大小的格子,叫做Region,每个JVM可以将内存划分我2048个Region,每个Region的大小就是内存大小/2048。其中E,S,O代表的分别是Eden区,Survivor区和Old区,其中Eden和Survivor的比例依然是8:1:1,新生代与老年代的占比会根据使用的情况动态调整,默认5%,最大超过60%,此外H代表的是直接分配大对象的Region,单个Region不够分配时垮多个连续Region分配。

3.7.2.G1的GC过程

主要分成以下几个步骤:

  • 初始标记:暂停用户线程(STW),找到GCRoots直接引用的对象。
  • 并发标记:通过GCRoots做可达性分析,标记所有存活对象。
  • 最终标记:暂停用户线程,修改标记在并发标记阶段,由于用户线程运行导致的标记变动的那一部分对象。
  • 筛选回收:暂停用户线程,对各个Region的回收价值进行排序,根据期望的GC停顿时间制定回收Region中失效的对象。

运行图示如下:
在这里插入图片描述
可以看到的是,前面的三个阶段与CMS类似,在筛选回收的阶段相对比CMS的并发清理,G1采用了暂停用户线程的回收方式。


那这里为什么将并发清理改成STW的筛选回收呢?
主要是两点原因:

①暂停用户线程,采用并行回收的方式大大提高了收集效率。
②可以在用户配置的期望停顿时间内进行回收,不会造成过大的停顿。

第②点是G1的特性,可以使用-XX:MaxGCPauseMillis来明确的指定一个停顿时间,默认是200ms,在这200ms内,根据回收价值排序的先后顺序,对各个region进行回收,停顿时间到后,即使没有将所有的垃圾都回收完,也会停止回收,转回用户线程继续执行程序,达到减少停顿时间的目的。

停顿时间的配置并不是越短越好,过短的停顿时间会导致Eden区频繁GC,而过大的停顿时间除了回收时体验不佳外,还可能导致Eden去占用内存的比例过高,存活对象过多后,超过了Survivor承受的界限,大量的对象直接进入了老年代,会增加更加耗时的Mixed GC回收频率。

3.7.3.G1回收的分类

前面提到了G1收集器是横跨在新生代和老年代之间的,G1保留了逻辑上分代的特征,回收的类型也有三种:

  • Young GC:在当前Eden区满了之后,会计算回收Eden中的内存有没有达到配置的停顿时间,比如200ms,这次回收的停顿时间远小于200ms时,会扩大Eden在整个内存中的比例,继续分配内存,直到下次Eden满了,并且停顿时间达到200ms时,会进行一次Young GC。
  • Mixed GC:老年代内存占用到达-XX:InitiatingHeapOccupancyPercen配置的比例后就会进行一次Mixed GC,回收的是整个Young区和部分Old区,Old区根据回收价值排序先后进行回收。此时主要使用的是复制回收算法,将存活的对象复制到空闲的Region中,如果复制的过程中发现空闲的Region不够了,就会发生一次Full GC。
  • Full GC:STW,采用单线程标记、整理内存,非常耗时。

3.7.4.垮代引用问题

G1中虽然将内存划分为一个个的Region,但是每个Region不能并不是孤立存在的,换句话说,每个Region中的对象可能持有其他某个Region中的对象引用,如果老年代中的对象引用了新生代中的对象,就是垮代引用,如果在对新生代GC的时候还需要去扫描老年代的内存,那Young GC的效率也会降低不少,每个收集器都有这样的问题,只是在G1中更加突出了。

那么怎么解决这个问题呢?

为了避免扫描整个堆,设计者在每个Region中都加入了一个叫Remember Set(RSet)的数据结构,在虚拟机发现对象中有写入Reference类型的数据时,会将引用信息写入到被引用对象所在RSett中,例如老年代的对象持有对新生代对象的引用时,将引用的信息写入到新生代对象的RSet中,在进行Young区的内存回收时,只需要验证RSet中是否有引用,而不需要扫描老年代。

4.如何选择垃圾收集器

如果对应用程序没有非常严格的停顿时间要求,尽可能的让服务器自行选择垃圾收集器,通过调整堆的大小、比例等参数来提高JVM的性能。如果调整后还是无法满足要求,可以参考下面的调整方案。

  • 内存小于100M选择串行收集器,-XX:+UseSerialGC。
  • 单核处理器并且对停顿时间没有要求,选择串行收集器。
  • 关注吞吐量的跑任务的服务,如果允许暂停时间大于1秒,选择并行收集器,-XX:+UseParallelGC。
  • 对停顿时间有要求不太关注吞吐量的B/S服务,选择并发的收集器,-XX:+UseConcMarkSweepGC 或 -XX:+UseG1GC。

5.JVM堆内存可视化工具

5.1.Visual VM的使用

在JDK的安装目录的bin文件夹下,找到jvisualvm.exe
在这里插入图片描述
打开后,左边有启动的Java应用程序列表,我这里选择一个idea的应用,查看堆的情况,可以非常直观的看到各个分区的内存及内存回收的情况。
在这里插入图片描述

5.2.安装Visual GC

在Visual VM中,选择工具->插件,在可下载的工具中找到Visual GC,并安装。
在这里插入图片描述
在这里插入图片描述
这里如果下载不成功给的话可以去,VisualVm 插件中心,选择对应自己安装版本的插件,我这里是jdk8。
在这里插入图片描述
点进去后选择Visual GC下载
在这里插入图片描述
下载完成后返回Visual VM的插件中,选择已下载->添加插件,找到刚刚下载的com-sun-tools-visualvm-modules-visualgc.nbm,加载完成后从已下载列表中找到Visual GC,选择安装。
在这里插入图片描述
安装完成后就可以愉快的玩耍了。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值