我是靠谱客的博主 光亮太阳,最近开发中收集的这篇文章主要介绍JVM学习笔记(二):垃圾回收,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

    • 1、如何判断对象是否可以回收
      • 1.1、引用计数法
      • 1.2、可达性分析算法
      • 1.3、四种引用
        • 1.3.1、软引用和弱引用
        • 1.3.2、虚引用和终结器引用
        • 1.3.3、总结
    • 2、垃圾回收算法
      • 2.1、标记清除算法
      • 2.2、标记整理算法
      • 2.3、复制算法
    • 3、分代垃圾回收机制
    • 4、垃圾回收器(7种)
      • 4.1、串行回收器(Serial + SerialOld)
      • 4.2、并行回收器(Parallel + ParallelOld)
      • 4.3、响应时间优先(ParNew + CMS)
      • 4.4、G1回收器
        • 4.4.1、G1回收阶段
        • 4.4.2、各阶段工作流程
        • 4.4.3、Young Collection 跨代引用
        • 4.4.4、重标记 Remark

1、如何判断对象是否可以回收

1.1、引用计数法

我们的对象是存放在堆空间当中,判断对象是否可以被回收,这里有两种不同的算法。
我们首先讲解第一种,引用计数法

只要一个对象被其他变量所有引用,就让该对象的计数 + 1,如果被引用了两次就让其计数变为2
如果某个变量不再引用它,计数就会 - 1,那么这个对象的引用计数变为0时,意味着没有变量对该对象进行引用
它就会作为一个垃圾,进行回收

但是该方式有一个弊端:循环引用!
在这里插入图片描述
如图所示,A对象引用B,此时B计数为1,B又引用了A,此时A计数变为1。这两个对象互相引用,没有别的对象引用它们,此时他们两个是否会被垃圾回收? 不会!

因为他们各自的引用计数是 1,虽然它俩都不会被使用,但是它们的引用计数不是0,所以不能被回收,这样就造成内存泄漏

引用计数法是在Python虚拟机中采用,然而Java虚拟机并没有采用该方法进行判断,JVM采用的是可达性分析算法

1.2、可达性分析算法

该算法是JVM采用判断方法,该算法首先需要确定根对象(GC Root),什么是根对象?

可以理解为肯定不能当成垃圾回收的对象
在垃圾回收之前,我们首先会对堆内存中所有对象进行扫描,看看对象是否直接或间接被根对象(GC Root)进行引用
如果是就不能被回收,反之就表示可以作为垃圾将来被回收

1.3、四种引用

面试时候会经常问道四种引用,例如:Java中的四种引用有哪些?

严格来说,并不是只有四种,而是有五种,这里我们会介绍五种:

  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用
  5. 终结器引用

在这里插入图片描述
注意:实线表示强引用

其实我们平时使用的所有引用都是强引用,比如我们new一个对象,此时变量就强引用了new出来得对象。

强引用的特点:就是沿着GC Root引用链找到对象,那么该对象就不会被垃圾回收,只有GC Root对该对象的引用断开,才会被垃圾回收

1.3.1、软引用和弱引用

软引用弱引用比较相似,跟强引用的区别是:

只要A2和A3没有被直接强引用,此时垃圾回收发生时,它们都可能被回收
例如:A2对象被C对象间接地强引用,同时被B对象进行强引用,这种情况不会被垃圾回收,当B对象不在对A2对象强引用,只有软引用引用A2对象,发生垃圾回收并且内存不够,此时就会把软引用引用的对象释放掉

弱引用跟软引用很像,没有强引用直接引用时,并且只要发生垃圾回收(不管内存是否不足) ,都会把弱引用的对象回收!

所以软引用弱引用的区别是(在没有GCRoot引用时):
软引用只有GC后内存仍然不足才会被回收
弱引用只要发生GC就会被回收,不管内存是否充足

实际场景:我们一般把不重要的资源通过软引用和弱引用方式进行引用,这样如果堆内存不足可以对引用的对象资源进行GC,提供内存空间。
这样软/弱引用本身也没有价值(不引用任何对象),就会放入引用队列中被回收,毕竟它们本身也是对象,会占用空间

软引用和弱引用可以配合引用队列一起工作,当软引用的对象被回收,软引用和弱引用本身就是对象,如果在创建时给它分配了一个引用队列,当软引用所引用的对象被回收时,软引用本身就会进入到队列,弱引用也是同理
在这里插入图片描述

为什么要做这样的处理?

因为不论是软引用还是弱引用,他们自身也要占用一定的内存,如果想要对他俩占用的内存做进一步释放,需要使用引用队列来找到他俩,做进一步处理,毕竟他俩还被C对象引用着。

1.3.2、虚引用和终结器引用

接下来介绍一下虚引用终结器引用,它俩与软引用、弱引用不同:
在这里插入图片描述

软引用和弱引用既可以跟引用队列一起使用,也可以不一起使用
而虚引用和终结器引用必须和引用队列一起使用!
所以虚终引用被创建时候,就会关联引用队列

当我们创建ByteBuffer实现类对象时,就会创建一个名为cleaner的虚引用对象,ByteBuffer会分配一块直接内存,并且把内存地址传递给虚引用对象,为什么要这么做?

将来ByteBuffer一旦没有强引用所引用,此时ByteBuffer将被垃圾回收,但是它分配的直接内存无法被Java垃圾回收管理,所以我们需要在ByteBuffer被回收时,让虚引用对象进入引用队列中。虚引用所在的队列会由一个叫做ReferenceHandler线程定时的到引用队列中寻找,看有没有新入队的Cleaner,如果有就会调用clean方法,该方法会根据前面记录的直接内存地址调用Unsafe.freeMemory(),这样才能把我们的直接内存释放掉!就不会导致直接内存泄露,这就是虚引用的作用

我们再来解读一下终结器引用:

我们都知道,所有的Java对象都会继承Object类,该类中有一个叫做finallize()终结方法,当我们的A4对象重写了该方法,并且没有强引用,此时就可以当作垃圾进行回收。

那么该方法何时被调用?
当没有强引用引用A4对象时,此时JVM会创建终结器引用,当A4对象将要被垃圾回收时,终结器引用会加入到引用队列中,此时A4还没有被垃圾回收,此时由一个优先级很低得线程FinallizeHandler查看引用队列中是否有终结器引用,如果有就会根据终结器引用找到需要垃圾回收的对象A4,并且调用A4重写的finallize()第二次垃圾回收时才会将A4对象进行回收,这就是终结器引用的作用

我们发现,finallize()工作效率很低,第一次回收时并不能直接释放,而是先要将终结器对象入队,而且处理队列的FinallizeHandler线程优先级很低,执行的机会很少,所以会造成该方法迟迟不能调用,对象占用的内存迟迟不能释放,这就是为什么不推荐使用finallize()释放资源的理由

1.3.3、总结

在这里插入图片描述

2、垃圾回收算法

具体如何进行垃圾回收,是需要垃圾回收算法,常见有三种:

  1. 标记清除
  2. 标记整理
  3. 复制

2.1、标记清除算法

在这里插入图片描述

标记清除算法分为两个阶段:

  1. 先标记,看看堆内存中哪些对象可以作垃圾(没有GC Root直接引用)
  2. 清除,把堆内存中垃圾对象所占用的对象给清除,只需要把对象所占用内存的起始、结束地址,放到空闲的地址列表中,下次分配对象时,就去地址列中查看是否有空闲的空间存放新对象

标记清除算法的优缺点

  • 优点是速度快,只需要把垃圾对象起始、结束地址做记录,不需要做额外处理,所以回收速度很快
  • 缺点是容易产生内存碎片,因为清除之后不会对空闲的内存空间做进一步整理工作,当存入的对象占用内存过大,由于内存碎片的特点不连续,导致没办法直接将对象存入,会造成内存溢出

2.2、标记整理算法

在这里插入图片描述
标记整理和标记清除在第一个阶段是一样的,区别主要是整理部分,

整理操作,避免之前出现的内存碎片问题,在清理过程中把不回收的对象向前移动,让内存空间紧凑,这样就没有标记清除算法中的缺点

优缺点

  • 优点:没有内存碎片
  • 缺点:由于整理过程牵扯到对象的移动,效率自然会变低,如果有些变量引用了我们移动的对象,此时引用地址会发生改变,会降低性能

2.3、复制算法

在这里插入图片描述
复制算法是将内存区域划分为大小相等的两块,分别是FROMTO,TO区域是空闲的

首先还是进行垃圾对象标记,然后将FROM区不被回收的对象复制到TO区域中,复制过程中会完成内存碎片整理。复制完成后FROM区都是垃圾对象,将所有垃圾对象全部清空,并且交换 FROM和TO区域位置。

复制算法优缺点

  • 优点:不会产生内存碎片
  • 缺点:占用双倍内存空间

3、分代垃圾回收机制

分代垃圾回收机制会将堆内存划分为两部分:

  • 新生代
  • 老年代

新生代划分为:

  • 伊甸园
  • 幸存区FROM
  • 幸存区TO

为什么要做区域划分?

主要是因为Java中有的对象需要长时间使用,这类对象存放到老年代
那些使用完可以被回收的对象就放入到新生代
这样就可以针对对象生命周期的特点进行不同的垃圾回收策略
老年代垃圾回收很久发生一次,新生代垃圾会收很频繁
不同区域采用不同算法,就会更有效管理垃圾回收区域


在这里插入图片描述

当我们创建新的对象时,默认采用伊甸园内存空间,当我们创建多个对象,伊甸园放不下了,此时就会触发一次垃圾回收,新生代的垃圾回收我们称之为Minor GC,触发后采用可达性分析算法判断哪些对象可以作为垃圾进行标记,标记成功后就会采用复制算法,把存活的对象复制到幸存去TO,然后将幸存的对象寿命 + 1,做完复制操作后会交换FROM和TO的位置。

重复上述操作伊甸园又满了,就触发第二次垃圾回收,不仅要判断伊甸园中对象是否需要回收,就连幸存区的对象也要进行判断是否需要进行回收。

幸存区中对象并不会永远存放于幸存区,当它的寿命超过默认阈值15,说明该对象价值很高,经常在使用,这样就没必要继续放到幸存区了,就会把该对象晋升到老年代中。因为老年代垃圾回收频率低,这种价值较高的对象就会从幸存区晋升到老年代

有这么一种情况:

晋升到老年代对象很多,新生代也快满了,此时新生代还要存入一个对象,此时存不下了。这时就会触发Full GC,触发垃圾回收,从新生代到老年代做一个整个垃圾回收操作,这就是基本流程

总结:

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命阈值是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,世界暂停(stop the world)的时间更长

特殊情况:当我们向新生代存放一个大对象,此时内存不够,即使发生了垃圾回收内存也不够直接存放大对象,如果老年代空间足够,这时就会把大对象直接晋升到老年代当中。

4、垃圾回收器(7种)

垃圾回收器可以分为三类:

  1. 串行
  2. 吞吐量优先
  3. 响应时间优先

特点

串行:

  • 单线程
  • 堆内存较小,单核CPU

吞吐量优先:

  • 多线程
  • 堆内存较大,需要多核CPU来支持
  • 让单位时间内,STW时间最短

响应时间优先:

  • 多线程
  • 堆内存较大,需要多核CPU来支持
  • 尽可能让单次STW时间最短

4.1、串行回收器(Serial + SerialOld)

在这里插入图片描述

串行垃圾回收器开启语句如图,该垃圾回收器分为两部分:

  1. Serial:工作在新生代,采用的回收算法是复制,新生代内存不足发生垃圾回收使用Serial完成Minor GC
  2. SerialOld:工作在老年代,采用的算法是标记整理,老年代内存不足发生垃圾回收使用SerialOld完成Full GC

回收过程

假设现在有多核CPU,如图所示。刚开始4个线程都在运行。这时发现堆内存不够,触发垃圾回收,首先要让这些线程在安全点停下,因为在垃圾回收过程中,可能对象的地址需要发生改变。为了保证安全使用这些对象的地址,需要先让线程达到安全点暂停,此时完成垃圾回收工作,就不会有其他线程进行干扰。
注意:由于SerialSerialOld都属于单线程垃圾回收器,因此只有一个垃圾回收线程在运行,当垃圾回收线程运行时,其他线程全部进行阻塞暂停。如果垃圾回收线程结束,其他用户线程恢复运行。

4.2、并行回收器(Parallel + ParallelOld)

在这里插入图片描述

使用吞吐量优先并行垃圾回收器需要使用图中第一条参数进行开启,这两个开关在1.8中默认是开启的。

UseParallelGC是新生代垃圾回收器,采用复制算法,UseParallelOldGC是老年代垃圾回收器,采用标记整理算法。单从算法上来看,跟我们之前介绍的串行垃圾回收器是一样的。
区别在于Parallel这个名词(并行),暗指这两个垃圾回收器都是多线程,值得一提的是,这两个只要开启其中一个,就会顺带把另一个进行开启。

回收过程

如图所示,现在有多核CPU,共四个线程都在运行。突然内存不足,触发一次垃圾回收,这些用户线程就会在安全点停下来。垃圾回收器会开启多个线程进行垃圾回收,垃圾回收线程的个数默认和CPU核数一致。
线程数可以通过第五条命令来控制。打开第二条命令时,吞吐量优先回收器工作时,就会动态的调整伊甸园跟幸存区的比例,包括整个堆空间大小、晋升阈值都会进行调整。

4.3、响应时间优先(ParNew + CMS)

在这里插入图片描述

开启参数在第一行,UseConcMarkSweepGC(CMS)是基于标记清除的垃圾回收器,工作于老年代,并且是并发的,并发是指垃圾回收器工作时,其他的用户线程也能同时进行,用户线程和垃圾回收线程是并发执行。
并行的含义是指垃圾回收器运行期间,工作线程不能运行(STW)。
CMS垃圾回收器某些时刻会出现并发效果。与之配合的UseParNewGC是工作于新生代垃圾回收器。
CMS垃圾回收器有些时刻会发生并发失败问题,此时会采取补救措施,CMS回收器会退化成SerialOld单线程垃圾回收器。

工作流程:

多个CPU并行执行,此时老年代发生内存不足,线程们都会到达安全点暂停,然后执行CMS垃圾回收器,CMS会执行初始标记动作,仍然需要STW,只会标记根对象,标记完成后用户线程恢复运行,与此同时垃圾回收线程进行并发标记,把剩余垃圾标记出来,此时跟用户线程是并发执行的,不需要STW,所以响应时间是很短的,并发标记后还要进行重复标记,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,又要STW。因为并发标记时用户线程也在工作。重新标记结束后用户线程又可以恢复运行,最后垃圾回收线程做并发清理

细节

  • 初始标记时,线程数受到途中第二条参数影响,根据我们的例子,n为4
  • 但是并发的GC线程数不一样,建议把第二条第二个参数设置为并行线程数的1/4
  • CMS垃圾回收器工作过程中,执行并发清理,由于其他用户线程还可以继续执行,此时运行过程中可能产生新的垃圾,所以并发清理同时不能把新的垃圾干掉,所以等到下一次垃圾回收清理。这些新垃圾我们把它叫做浮动垃圾,需要等到下次做垃圾回收时才能清理掉。所以我们需要预留空间保存浮动垃圾,我们途中第三条参数就是用来控制何时进行CMS垃圾回收时机。如果percent赋值为80,那就是当老年代内存占用达到80%,就执行一次垃圾回收,为了预留空间给浮动垃圾
  • 在我们重新标记阶段有一个特殊的场景,有可能新生代对象会引用老年代对象,如果此时进行重新标记,必须扫描整个堆内存,这样对性能影响非常大。 我们可以使用最后一条参数避免这种情况,在做重新标记之前,先对新生代做垃圾回收,这样新生代存活对象少了,将来扫描的对象就少了,这样就会减轻重新标记时压力
  • CMS有个特点:在内存碎片较多情况下,会造成将来分配对象时新生代和老年代空间都不足,这样就会造成并发失败,此时CMS老年代垃圾回收器不能正常工作,此时CMS会退化为SerialOld,做一次单线程串行垃圾回收进行整理,这样碎片减少了才能进行工作

4.4、G1回收器

使用场景

  • 同时注重高吞吐和低延迟,默认暂停目标是200ms
    超大堆内存,会将堆划分为多个大小相等的Region
    整体上是标记整理算法,两个区域之间是复制算法

4.4.1、G1回收阶段

在这里插入图片描述
G1垃圾回收分为三个:

  1. Young Colletion:新生代垃圾收集
  2. Young Colletion + Concurrent Mark:新生代垃圾收集 + 并发标记
  3. Mixed Colletion:混合收集

这三个阶段是个循环过程,最开始是新生代垃圾收集,当老年代内存超过阈值,会在新生代垃圾收集的同时进行并发标记,该阶段完成之后会进行混合收集,会对新生代幸存区和老年代都来进行规模较大的收集,混合收集结束后,再次进入新生代垃圾收集过程…

4.4.2、各阶段工作流程

Young Colletion
在这里插入图片描述
G1垃圾回收器会把整个堆内存划分为无数等大区域,每个区域都可以独立作伊甸园、幸存区、老年代。
刚开始区域都是空闲的,新创建的对象会分配到伊甸园(E),当伊甸园区逐渐被占满就会触发新生代的垃圾回收,触发STW
在这里插入图片描述
新生代垃圾回收后,会把幸存的对象复制算法放入幸存区(S)
在这里插入图片描述
当幸存区对象也比较多,或者存存货年龄超过阈值,此时又会发生垃圾回收,幸存区部分对象会晋升到老年代,不够年龄的会复制到另一个幸存区

Young Colletion + CM
在这里插入图片描述
我们进行垃圾回收时,会将对象进行初始标记并发标记,初始标记就是标记根对象,并发标记是从根对象出发顺着引用链找到其他标记对象。
初始标记在新生代GC时就发生了,并发标记是当老年代占用堆空间比例达到一定的阈值会发生并发标记

Mixed Colletion
在这里插入图片描述
该阶段,伊甸园区对象会通过复制算法复制到幸存区中,包括不够年龄的幸存区对象也会复制到幸存区,符合晋升条件的对象会晋升到老年代区
还有一部分老年代的区域,发现里面一些对象没用了,老年代也采用复制算法复制到新的老年代区域(红色箭头),图中参数会根据最大暂停实际按有选择的进行回收。因为堆内存空间太大了,老年代垃圾回收时间可能比较长,达不到最大暂停时间这个目标。为了达到这个目标,G1就会从老年代选出回收价值最高的区进行垃圾回收,这样就能达到暂停时间。
所以混合收集阶段,会优先收集垃圾最大的区域,目的就是达到暂停时间短的目的。

4.4.3、Young Collection 跨代引用

在这里插入图片描述
首先找到根对象,根对象进行可达性分析找到存活对象,存活对象进行复制到幸存区。

根对象有一部分来自于老年代,老年代存活对象很多,如果遍历整个老年代效率很低。所以将老年区再次细分,每个card大约是512k,如果老年代有一个对象引用新生代对象,对应的card标记为dirty card,这样就不需要遍历整个老年代了,缩小了搜索范围。

在这里插入图片描述
该图中粉色区域为dirty card区,新生代这边有Remeber Set,会记录外部引用(都有哪些脏卡),将来对新生代进行垃圾回收时,就可以通过Remeber
Set 知道对应了那些脏卡,然后去脏卡区遍历GC Root

这里有个问题:我们需要标记脏卡,但是当引用发生变更时都要去更新脏卡,这是异步操作,不会立刻完成脏卡更新。会把更新指令放入到dirty card queue中,将来由线程完成脏卡更新操作!

4.4.4、重标记 Remark

在这里插入图片描述
上面总是提到过并发标记、重新标记这两个名词阶段,说白了就是Remark阶段,接下来介绍一下Remark阶段相关知识

上面这个图表示并发标记阶段时对象的处理状态。
图中黑色表示已经处理完成的,并且被引用了,说明黑色不会被垃圾回收。
灰色表示正在进行处理,白色是尚未处理的。灰色如果最终被处理完成,他就会最终变成黑色。右下角那个白色由于也有引用,因此它最终也会变成黑色存活下来。

思考个问题

在这里插入图片描述

如果处理到B,因为有强引用,所以就变成黑色。如果处理到C,由于我们这个标记是并发,意味着同时也有用户线程将对象引用就行修改,比如将B和C之间强引用断开,此时处理完B处理C,发现C和B之间已经没有联系了,处理到C进行标记,C为白色。
真个并发标记结束之后,C对象仍然是白色,会被回收掉,这是第一种情况
在这里插入图片描述

在这里插入图片描述
另一种情况:在C和B处理完之后,并发标记可能还没结束,用户线程又改变了C地址,比如说把C对象当成A对象的属性,做一次赋值操作。因为C之前处理过了,认为已经是白色的。等到整个并发标记结束后,仍然认为C是垃圾,就会被回收了,这样就不对了,因为C被强引用了,不该被回收。
所以需要对对象的引用做进一步检查,就是我们提到的Remark重新标记阶段,防止该现象的发生。
具体操作:当对象引用发生改变,JVM就会加入一个写屏障,只要C对象引用发生改变,写屏障功能就执行,会把C加入到队列中,把C变成灰色,整个并发标记结束,进入重新标记阶段,STW。重新标记线程就会把队列中对象取出来再次检查,发现是灰色的,要对它做进一步判断处理,发现强引用,所以还是要把它变成黑色。这样C不会被误当成垃圾回收掉

最后

以上就是光亮太阳为你收集整理的JVM学习笔记(二):垃圾回收的全部内容,希望文章能够帮你解决JVM学习笔记(二):垃圾回收所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部