JVM垃圾收集(GC)器—垃圾收集

垃圾收集算法

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类。由于引用计数式垃圾收集算法在主流Java虚拟机中均未涉及,因此主要介绍追踪式垃圾收集的算法。

1、标记—清除法(Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记阶段也就是垃圾的判断过程。

在这里插入图片描述
缺点:
标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

实现流程:
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
在这里插入图片描述

2、标记—复制算法(Mark-Copying)

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。

在这里插入图片描述
(1)优点
① 这种算法实现简单,运行高效且不容易产生内存碎片

(2)缺点
① 对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半
② Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低

实现流程:
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
在这里插入图片描述

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

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动(记住是完成标记之后,先不清理,先移动再清理回收对象),然后清理掉端边界以外的内存

在这里插入图片描述
(1)优点:
标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端

(2)缺点:
标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多

(3)改进
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

实现流程:
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
在这里插入图片描述

4、分代收集算法

分代收集算法分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般是把Java堆分为新生代和老年代:
在新生代(Minor GC)中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代(Full GC 或 Major GC)中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收

新生代与老年代、永久代

JVM中的堆,一般分为两部分:新生代和老年代。
在这里插入图片描述

1、新生代

主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收

(1)新生代又分为 Eden区、ServivorFrom、ServivorTo 三个区。

① Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
② ServivorTo:保留了一次MinorGC过程中的幸存者。
③ ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

(2)当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。

MinorGC的过程:采用复制算法
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
在这里插入图片描述

2、老生代

主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC 前一般都先进行了 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间

MajorGC 过程:采用标记—清除算法
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM (Out of Memory)异常。

3、永久代

指内存的永久保存区域,主要存放Class 和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代

元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

类的元数据放入native memory,字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

GC 的类型

GC 有 Minor GC、Major GC 和 Full GC 类型。
在这里插入图片描述

1、Minor GC

(1)只收集新生代

(2)触发机制:当新生代的 Eden 区满的时候,JVM 会触发 MinorGC(注: surviver 区满时不会触发 MinorGC,但是 MinorGC 执行时会对 Eden 区和 surviver 区同时进行回收)。
在这里插入图片描述
(3)回收算法:复制算法

1、Major GC(Full GC)

(1)只收集老年代

(2)触发机制:当新生代的对象进入老年代导致老年代空间不足时就会触发 MajorGC;当无法找到足够大的连续空间分配给新创建的较大对象时(如大数组),也会触发 MajorGC 进行垃圾回收腾出空间。。
在这里插入图片描述
(3)回收算法:标记—清除或者标记—整理算法

1、Full GC

(1)收集新生代、老年代和永久代

(2)触发机制:① 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行;② 老年代空间不足; ③方法区空间不足;④通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存;⑤ 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时;⑥ 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载;
在这里插入图片描述

内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代中(大对象直接分到老年代),分配的规则并不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
在这里插入图片描述

1、对象优先在Eden分配

大多数情况下,对象会在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机会发起一次 Minor GC。Minor GC相比Major GC更频繁,回收速度也更快。通过Minor GC之后,Eden区中绝大部分对象会被回收,而那些存活对象,将会送到Survivor的From区(若From区空间不够,则直接进入Old区) 。

Survivor区

Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区中存活的对象放到 Survivor 的 From 区,而在 From 区中,仍存活的对象会根据他们的年龄值来决定去向。(From Survivor 和 To Survivor 的逻辑关系会发生颠倒: From 变 To , To 变 From,目的是保证有连续的空间存放对方,避免碎片化的发生)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Survivor区存在的意义

如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代

2、大对象直接进入老年代

所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 “安置” 它们。

虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)

3、长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中(正常情况下对象会不断的在Survivor的From与To区之间移动),并且对象年龄设为1。对象在Survivor区中每经历一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 XX:MaxPretenuringThreshold 设置

4、动态对象年龄判定

为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到 MaxPretenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代,无需等到MaxPretenuringThreshold中要求的年龄。

对象分配中的 TLAB

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

总结

在这里插入图片描述
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值