七种垃圾收集器和垃圾回收、分代收集、GCROOTS相关概念、GC如何判断一个对象可以被回收

垃圾收集器概述

在这里插入图片描述

新生代收集器(全都是复制算法):serial、parnew、parallel scavenge
老生代收集器:cms(标记-清除算法)、serial old(标记-整理)、paralle old(标记整理)
整堆收集器:g1(一个region中是标记-清除算法,二个region是复制算法)
paralle:并行,多个垃圾收集器并行工作,此时用户线程处于等待状态
并发(concurrent):用户线程和垃圾收集器同时执行
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

JDK诞生之后第一个垃圾回收器就是Serial,和Serial Old。追随提高效率,诞生了PS,为了配合CMS.诞生了PN.CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS。所谓的Serial指的是单线程。Parallel Scavenge指的是多线程。常见的垃圾回收器组合最常用的是有三种(Serial+Serial Old)、(Parallel Scavenge+ParallelOld)、(ParNew+CMS)。

在这里插入图片描述
看图中画的红线,但凡是能连接在一起的都可以组合。前面几种不仅都是在逻辑上分年轻代和老年代,在物理上也是分年轻代和老年代的。G1只是在逻辑上分年轻代老年代,在物理上他就分成一块一块的了,另外需要注意的是CMS还有一个组合是和Serial Old组合到一起的。

下面解释一些垃圾收集中相关的概念

垃圾回收算法

1)标记—清除算法(Mark-Sweep)(DVM 使用的算法)

标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法 后续的所有算法都是对其的不足进行改进得到的,标记和清除阶段的效率不高,而且清除后会产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
在这里插入图片描述
在这里插入图片描述

2)复制算法(Copying)

复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1)。

3)标记—整理算法(Mark-Compact)

标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。
在这里插入图片描述

4)分代收集(Generational Collection)

分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块,一般java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法

什么是Stop The World ?

进行垃圾收集时,必须暂停其他所有工作线程,Sun将这种事情叫做"Stop The World”

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用 户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。

为什么需要 STW?

在 java 应用程序中「「引用关系」」是不断发生「「变化」」的,那么就会有会有很多种情况来导致「「垃圾标识」」出错。想想一下如果 Object a 目前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他对象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果没有 STW 就要去无限维护这种关系来去采集正确的信息。再举个例子,到了秋天,道路上洒满了金色的落叶,环卫工人在打扫街道,却永远也无法打扫干净,因为总会有不断的落叶。

垃圾标记阶段?

在GC执行垃圾回收之前,为了区分对象存活与否,当对象被标记为死亡时,GC才回执行垃圾回收,这 个过程就是垃圾标记阶段。

GC如何判断一个对象可以被回收

引用计数法(已被淘汰的算法)
  1.每一个对象有一个引用属性,新增一个引用时加一,引用释放时减一,计数为0的时候可以回收。但是这种计算方法,有一个致命的问题,无法解决循环引用的问题

可达性分析算法(根引用)
 1.从GcRoot开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GcRoot没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就可以判定回收。
 2.那么GcRoot有哪些?
  1.虚拟机栈中引用的对象
  2.方法区中静态属性引用的对象
  3.方法区中常量引用的对象
  4.本地方法栈中(即一般说的native方法)引用的对象不同的引用类型
不同的引用类型的回收机制是不一样的
  1.强引用:通过关键字new的对象就是强引用对象,强引用指向的对象任何时候都不会被回收,宁愿OOM也不会回收

2.软引用:如果一个对象持有软引用,那么当JVM堆空间不足时,会被回收。如果内存空间足够则不会被回收。一个类的软引用可以通过iava.lang.ref.SoftReference持有
  3.弱引用:如果一个对象持有弱引用,那么在G时,只要发现弱引用对象,就会被回收。一个类的弱引用可以通过java.lang.ref.WeakReference持有。弱引用与软引用的区别:弱引用拥有更短的生命周期,在垃圾回收器线程扫描它所管辖的内存区域中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够都会回收它的内存,不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
  4.虚引用:几乎和没有一样,随时可以被回收。通过PhantomReference持有 。虚引用主要用来跟踪对象被垃圾回收的活动。在程序设计中一般很少用弱引用与虚引用,软引用的情况较多,因为软引用可以加速jvm内存回收速度,可以维护系统的运行安全,防止内存溢出(OOM)问题的产生

垃圾回收器是怎样寻找 GC Roots 的?

我们在前面说明了根可达算法是通过 GC Roots 来找到存活的对象的,也定义了 GC Roots,那么垃圾回收器是怎样寻找GC Roots 的呢?首先,「「为了保证结果的准确性,GC Roots枚举时是要在STW的情况下进行的」」,但是由于 JAVA 应用越来越大,所以也不能逐个检查每个对象是否为 GC Root,那将消耗大量的时间。一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 GC 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 「「OopMap」」 的数据结构来记录这类信息。

如何判断一个常量是废弃常量

运行时常量池主要回收的是废弃的常量,假如在常量池中存在字符串“abc"如果当前没有任何string对象引用该字符串,说明”abc"是废弃常量,这时如果发生内存回收的话而且有必要的话“abc"就会被系统清理出常量池

新生代 年老代

由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为From Survivor 和 To Survivor)、Tenured
1.为什么会有年轻代

我们先来缕一缕,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.年轻代中的GC

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

3.一个对象的这一辈子

我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

4.有关年轻代的JVM参数

1)-XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁

JVM的永久代中会发生垃圾回收么?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的native内存区)

1.Serial

Serial是一个「「单线程」」的垃圾回收器,「「采用复制算法负责新生代」」的垃圾回收工作,可以与 CMS 垃圾回收器一起搭配工作。,单线程的含义在于它会stop the world。垃圾回收时需要stop the world,直到它收集结 束。所以这种收集器体验比较差。

在 STW 的时候「「只会有一条线程」」去进行垃圾收集的工作,所以可想而知,它的效率会比较慢。但是他确是所有垃圾回收器里面消耗额外内存最小的,没错,就是因为简单。
这个收集器新生代用复制算法,老年代用标记整理算法。
在这里插入图片描述

2.Serial Old 收集器

老年代单线程收集器,Serial收集器的老年代版本;Serial Old 收集器是在 TenuredGeneration 老年代上实现收集的,Serial Old 收集器 所使用的垃圾回收算法是标记-压缩-清理算法。在回收阶段,将标记对象越过堆的空闲区移动到堆的另一端,所有被移动的对象的引用也会被更新指向新的位置。

3.ParNew

ParNew 是一个「「多线程」」的垃圾回收器,「采用复制算法负责新生代」的垃圾回收工作,可以与CMS垃圾回收器一起搭配工作。

它其实就是 Serial 的多线程版本,主要区别就是在 STW 的时候可以用多个线程去清理垃圾。除了使用采用并行收回的方式回收内存外,其他行为几乎和Serial没区别。
可以通过选项“-XX:+UseParNewGC”手动指定使用ParNew收集器执行内存回收任务。

4.Parallel Scavenge

在这里插入图片描述

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector),是一个新生代收集器,也是复制算法的收集器,同时也是多线程并行收集器,与PartNew不同是,它重 点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU用于运行用户代码的时间/CPU总消耗时 间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
他可以通过2个参数精确的控制吞吐量,更高效的利用cpu。
分别是:-XX:MaxCcPauseMillis 和-XX:GCTimeRatio
新生代用复制算法,老年代用标记整理算法。

5.Parallel Old

Parallel Old 收集器:Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。JDK 1.6中才开始提供。

ParNew,其实就是Parallel New的意思,和Parallel Scavenge没什么区别,就是它的新版本做了
一些增强以便能让它和CMS配合使用,CMS某一个特定阶段的时候ParNew会同时运行。所以这个才是第三个诞生的。 我工作的时候,其余线程不能工作,必须等GC回收器结束才可以

6.CMS收集器?

CMS非常重要,因为它诞生了一个里程碑式的东西。原来所有的垃圾回收就是在我干活儿的时候你其它的不能干活儿。我垃圾回收器来了,其它所有工作的线程都得给我停等着我回收,我走了你才能继续工作。CMS的诞生就消除了这种疑问。CMS毛病非常多。以至于目前任何jdk版本默认都不是CMS。

Concurrent Mar Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。重视服务的响应速 度,希望系统停顿时间最短。采用标记-清除的算法来进行垃圾回收。
CMS垃圾回收的步骤?
初始标记(stop the world)
并发标记
重新标记(stop the world)
并发清除
在这里插入图片描述

第一个阶段叫做CMS initial mark(初始标记阶段)。很简单,就是我直接找到最根上的对象,其他的对象我不标记,直接标记最根上的
第二个是CMS concurrent mark(并发标记),据统计百分之八十的GC的时间是浪费在这里,因此它把这块最浪费时间的和我们的应用程序同时运行,对客户来说感觉可能是慢了一些,但至少你还有反应。并发标记。就是你一边产生垃圾,我这一边跟着标记但是这个过程是很难完成的。
所以最后又有一个CMS remark(重新标记)。这又是一个STW。在并发标记过程中产生的那些新的垃圾
在重新标记里头给它标记一下,这个时候需要你们俩停一下,时间不长。
最后是一个concurrent sweep(并发清理)的过程。并发清理也有它的问题,并发清理过程也会产生新的垃圾,这个时候的垃圾叫做浮动垃圾,浮动垃圾就得等着下一次CMS再一次运行的过程把它给清理掉。

初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记就是进行Gc Roots Tracing的过程。
重新标记则是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象的 标记记录,这个阶段停顿时间一般比初始标记时间长,但是远比并发标记时间短。
整个过程中并发标记时间最长,但此时可以和用户线程一起工作。
CMS收集器优点?缺点?
优点:

并发收集,低停顿

理由:由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
1、CMS收集器对CPU资源非常敏感

在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)

2、 CMS处理器无法处理浮动垃圾

CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法在当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” 。

3、CMS是基于“标记–清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。

空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。

为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了。
从线程的角度理解 ,它垃圾回收的线程和工作线程同时进行,叫做concurrent mark sweep
(concurrent 并发)。不管你用几个线程进行垃圾回收这个过程都太长了。在内存比较小的情况下,没有问题,速度很快。但是现在的服务器内存越来越大,大到什么程度,原来是一个房间,现在可以看成一个天安门广场。作为一个这么大的内存无论你多少个线程来清理一遍也得需要特别长的时间。以前大概在有10G内存的时候他用PS+PO停顿时间清理一次,大概需要11秒钟。 有人用CMS,这个最后也会产生碎片之后产生FGC,FGC默认的STW最长的到10几个小时,就直接卡死在哪儿了,大家什么都干不了。

什么条件触发CMS呢?老年代分配不下了,处理不下会触动CMS。初始标记是单线程,重新标记是多线程。
CMS的缺点:cms出现问题会调用Serial Old老年代出来使用单个线程进行标记压缩

7.G1收集器?

Garbage First收集器是当前收集器技术发展的最前沿成果。jdk 1.6_update14中提供了 g1收集器。
G1收集器是基于标记-整理算法的收集器,它避免了内存碎片的问题。
可以非常精确控制停顿时间,既能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集 上的时间不多超过N毫秒,这几乎已经是实时java(rtsj )的垃圾收集器特征了。
G1收集器是如何改进收集方式的?
极力避免全区域垃圾收集,之前的收集器进行收集的范围都是整个新生代或者老年代。而g1将整个Java 堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域垃圾堆积程度,维护一 个优先级,每次根据允许的收集时间,优先回收垃圾最多的区域。从而获得更高的效率。

G1与CMS两个垃圾收集器的对比

细节方面不同
1.G1在压缩空间方面有优势。
2.G1通过将内存空间分成区域( Region)的方式避免内存碎片问题。
3.Eden, Survivor,Old区不再固定、在内存使用效率上来说更灵活。
4.G1可以通过设置预期停顿时间( Pause Time)来控制垃圾收集时间避免应用雪崩现象。
5.G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stopthe world)的时候做。
6.G1会在 Young GC中使用、而CMS是老年代并发垃圾收集器

吞吐量优先:G1
响应优先:CMS
CMS的缺点是对cpu的要求比较高。G1是将内存化成了多块,所有对内段的大小有很大的要求
CMS是清除,所以会存在很多的内存碎片。G1是整理,所以碎片空间较小。

如何选择垃圾收集器?

  1. 如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。
    参数: -XX:+UseSerialGC 。
  2. 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。
    参数: -XX:+UseSerialGC 。
  3. 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。
    参数: -XX:+UseParallelGC 。

4.如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择G1 、ZGC 、CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
参数:
-XX:+UseConcMarkSweepGC 、
-XX:+UseG1GC 、
-XX:+UseZGC 等。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值