JVM 垃圾收集器

1.判断是否是存活对象?

1.1 引用计数算法

引用计数算法的基本思路

  • 在对象中添加一个引用计数器;
  • 每当有一个地方引用它的时候,计数器就加+1;
  • 每当有一个引用失效的时候,计数器就减-1;
  • 当计数器的值为0的时候,那么该对象就是可被GC回收的垃圾对象

注意:引用计数算法存在的问题:对象循环引用

a对象引用了b对象,b对象也引用了 a对象,a、b对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是0,所以引用计数算法就无法回收它们。

1.2 可达性分析算法

可达性分析算法( Reachability Analysis )基本思路:通过定,开义了一系列称为"GCRoots"的根对象作为起始节点集,从GC Roots始,根据引用关系往下进行搜索,查找的路径我们把它称为"引用链"。当个对象到GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象

可达性分析算法 也是JVM 默认使用的寻找垃圾算法。

1.3.Java 中的四种引用类型
1.3.1.强引用(Strong Reference)

强引用 是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器 绝不会回收它。当内存空间不足 时,JVM 宁愿抛出0utofMemoryError错误,使程序异常终止 ,也不会靠随意回收 具有强引用的对象来解决内存不足的问题。

强引用 :

java 复制代码
Object strongReference =new Object();

如果强引用对象不使用时,需要弱化从而使GC能够回收。

弱化方式1:显式地设置 strongReference 对象为null,则gc认为该对象不存在引用,这时就可以回收这个对象。但是,具体什么时候收集这要取决于 GC 算法。例如, strongReference 是全局变量时就需要在不用这个对象时赋值为 null ,因为强引用不会被垃圾回收。

强引用弱化方式1:

java 复制代码
strongReference =null;

应用场景 :在 ArrayList 集合类中定义 elementData 数组,在调用 clear()方法清空集合元素时,将每个数组元素被赋值为 null。目的是为了将内存数组 中存放的引用类型 进行内存释放 ,可以及时释放内存。不选择将 elementData=null,是为了避免在后续调用 add()等方
法添加新元素时,需要进行内存的重新分配

java 复制代码
public void clear(){
    modCount++;
    // clear to let Gc do its work
    for(inti=0;i<size; i++)
        elementDatali]=null
    size = 0;
}

强引用弱化方式2:

应用场景 :在一个方法的内部 有一个强引用 ,这个引用保存在 vM stack 中(Gc Root ),而真正的引用对象( object )保存在 中。当这个方法运行完成 后,就会退出方法栈,则这个对象会被回收。

java 复制代码
public void test(){
    Object strongReference =new object();
    //省略其他操作
}
1.3.2.软引用(Soft Reference)

创建软引用 ,可以使用Soft Reference

如果一个对象只具有软引用 ,则内存空间充足 时,垃圾回收器不会回收 它;如果内存空间不足 了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可用来实现内存敏感的高速缓存。

java 复制代码
//定义和访问软引用

//强引用
String strongReference = new String("abc");

// 软引用
String str = new String("abc");
SoftReference<String>softReference = new SoftReference<String>(str);

// 访问软引用
softReference.get();

软引用对象是在jvm 内存不够的时候才会被回收,我们调用 system.gc()方法只是起通知作用,最终何时回收,由JVM决定。

所以,当内存不足时, JVM首先将软引用 中的对象 引用置为 null然后通知垃圾回收器进行回收:

java 复制代码
//回收软引用

//软引用
String str = new String("abc");
SoftReference<string>softReference = new SoftReference<>(str);

str = null;

// NotifyGC
System.gc();

try {
    byte[]buff1 = new byte[900000000];// 内存充沛
    // byte[]buff2 = new byte[900000000];// 内存不足
catch(Error e){
    e.printstackTrace();
}
System.out.println(softReference.get());//abc 或null

应用场景: 短视频APP中的视频缓存,后退时,显示的短视频内容是重新进行请求还是从缓存中取出呢?
1.如果一个短视频在播放结束时,就进行内容的回收,则后退查看前面播放的短视频时,需要重新请求。
2.如果将播放过的短视频存储到内存中,会造成内存的开销,甚至会造成内存溢出。
此时,可以使用软引用解决这个实际问题;

1.3.3.弱引用(Weak Reference)

创建弱引用 ,使用WeakReference

只具有弱引用 的对象拥有更短暂的生命周期 。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用 的对象,不管当前内存空间足够与否 ,都会回收它的内存。

java 复制代码
String str = new String("abc");
SoftReference<string>softReference = new SoftReference<>(str);

str = null;

// NotifyGC
System.gc();

// 一旦发生GC,弱引用一定会被回收
System.out.println(weakReference.get());
1.3.4.虚引用(Phantom Reference)

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收。
虚引用,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。
在 JDK1.2之后,用PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()方法,而且它的 get()方法仅仅是返回一个 nu11,也就是说将永远无法通过虚引用来获取对象。

1.3.5.小结

2.垃圾收集算法

2.1.分代收集理论

目前主流 JvM 虚拟机中的垃圾收集器都遵循分代收集理论:

  • 弱分代:绝大多数对象都是朝生夕灭
  • 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡

按照分代收集理论 设计的**"分代垃圾收集器"** ,所采用的设计原则:

收集器应该将 Java 堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。

2.1.1.分代存储

如果一个区域中大多数对象都是朝生夕灭(新生代 ),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
如果一个区域中大多数对象都是难以回收(老年代 ),那么把它们集中放在一起, JM 虚拟机就可以使用较低的频率,来对这个区域进行回收。
这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。

2.1.2.分代收集

堆区按照分代存储的好处:
在 Java 堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有 minorGc、MajorGc、FullGC等垃圾收集类型划分。
在 Java 堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法、标记-清除算法、标记-整理算法等。
垃圾收集类型划分:

  • 部分收集( Partial Gc ):没有完整收集整个 Java 堆的垃圾收集,其中又分为:
    • 新生代收集(Minor Gc/YoungGc )
    • 老年代收集(Major Gc /0ld Gc)
    • 混合收集(Mixed Gc ):收集整个新生代和部分老年代的垃圾收集。
  • 整堆收集(Full GC ):收集整个 Java 堆的垃圾收集。
2.2.垃圾收集算法
2.2.1.标记-清除算法(Mark-Sweep)

"标记-清除算法"实现思路:

该算法分为"标记"和"清除"阶段:从根集合(GC Roots)开始扫描,标记出所有存活对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
"标记-清除"算法会带来两个明显的问题:
1.执行效率不稳定问题 :如果执行垃圾收集的区域,大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低。
2.内存空间碎片化问题:标记清除后会产生大量不连续的碎片,空间碎片太多,会导致分配较大对象时,无法找到足够的连续空间,从而会触发新的垃圾收集动作。

2.2.2.标记-复制算法(Copying )

"标记-复制"算法实现思路:

标记-复制"收集算法简称"复制算法",为了解决"标记-清除"面对大量可回收对象时执行效率低下的问题。

该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把已使用的空间一次清理掉。
"标记-复制"算法特点:
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法仅需要复制少数存活对象而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。
"标记-复制"算法的问题:
1.对象存活率较高,需要进行较多的内存间复制,效率降低
2.浪费过多的内存, 使现有的可用空间变为原先的一半

2.2.3.标记-整理算法(Mark-Compact )

"标记-整理"算法实现思路:
标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动,然后直接清理边界以外的内存,这样清理的机制,不会像标记-整理那样留下大量的内存碎片。

2.3.综上所述

当前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域使用不同的垃圾收集算法

例如: Heap 堆分为新生代老年代 ,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
在新生代中,每次收集都会有大量垃圾对象被回收,所以可以选择"标记-复制 "算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
在老年代中,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以选择"标记-清除 "或"标记-整理"算法进行垃圾收集,

3.垃圾收集器

3.1.Serial 收集器(新生代)

Serial (串行)收集器是最基本、历史最悠久的垃圾收集器,采用**"标记-复制"**算法负责新生代的垃圾收集。它是 Hotspot 虚拟机运行在客户端模式下的默认新生代收集器。

它是一个单线程收集器 。它会使用一条垃圾收集线程去完成垃圾收集工作,并且它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程("stop The World"),直到收集结束。

这样的设计,带来的好处就是:简单高效。对于内存资源受限制的环境,它是所有收集器中额外内存消耗最小的收集器。适合单核处理器或处理器核心数较少的环境,每次收集几十 MB 甚至一两百 MB 的新生代内存,垃圾收集的停顿时间完全可以控制在十几毫秒或几十毫秒,最多一百多毫秒。

3.2.Serial Old 收集器(老年代)

Serial 0ld 收集器同样是一个单线程 收集器,采用"标记-整理 "算法负责老年代的垃圾收集,主要用于客户端模式下的Hotspot虚拟机使用。
如果在服务器端使用,它主要有两种用途
1.在 JDK5 及以前版本,与 Parallel scavenge 收集器搭配使用
2.作为 CMS 收集器发生失败时的后备预案

3.3.ParNew 收集器(新生代)

ParNew 收集器是一个多线程 的垃圾收集器。它是运行在 Server模式下的虚拟机的首要选择,可以与 Serialold,CMS垃圾收集器一起搭配工作,采用"标记-复制"算法。

3.4.Parallel Scavenge 收集器(新生代)

Parallel Scavenge 收集器是也是一款新生代收集器,使用"标记-复制 "算法实现的多线程 收集器
Parallel Scavenge收集器预其它收集器的目标不同,它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

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

3.5.Parallel Old 收集器(老年代)

Parallel 0ld 收集器是一个多线程 的垃圾收集器,使用"标记-整理 "算法,是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量或者处理器资源较为稀缺的应用场景,都可以优先考虑Parallel Scavenge 收集器+ Parallel 0ld 收集器这个收集器组

3.6.CMS 收集器(老年代)

CMS(Concurrent Mark sweep )收集器是一种以获取最短回收停顿时间 为目标的收集器,基于"标记-清除"算法实现,是 Hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

目前很大一部分的 ava 应用集中在互联网站点或者基于浏览器的 B/S架构的服务器务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来自好的交互体验。所以,CMS收集器非常符合这类应用的收集场景。

工作流程
整个过程包括四个步骤
1.初始标记 (CMS initial mark):标记-下 GC Roots 能直接关联到的对象,速度很快;
2.并发标记 (CMS concurrent mark):从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
3.重新标记 (CMS remark):重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间长,远远比并发标记阶段时间短
4.并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

优点和缺点
主要优点:并发收集、低停顿
主要缺点:
影响用户线程的执行效率 :并发标记和并发清除时,是和用户线程起运行的,收集过程中肯定占用了用户程序的CPU 资源。CMS 默认启动的回收线程数是(cPU数量+3)/4,当CPU数量在 4个以上时,垃圾回收线程占用不少于 25%的CPU 资源,势必影响用户线程的执行效率。
无法处理浮动垃圾 :在并发清除阶段,用户线程并没有停止,所以还会继续产生新的垃圾,只能等待下一次收集时才能进行回收,这部分垃圾被称为"浮动垃圾
产生大量空间碎片:因为CMS收集器是基于"标记-清除"算法实现的,所以在进行大量的垃圾回收时,会产生很多不连续的内存空间。这是使用"标记-清除"算法都会有的缺点。

3.7.G1收集器(老年代)

什么是G1 垃圾收集器
G1( Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器、大容量内存的机器。它不再严格按照分代思想进行垃圾回收。G1 采用局部性收集的设计思路和基于 Region 的内存布局形式。
G1 垃圾收集器的结构
G1 采用局部性收集的思想 ,对于堆空间的划分,采用 Region 为单位的内存划分方式:
G1 垃圾回收器把堆划分成 2048 个大小相同的独立区域( Region),每个 Region 的大小取值范围是1MB-32MB且应为2的N次幕,即1MB,2MB,4MB,8MB,16MB32MB。
每个 Region 都会代表某一种角色,H、S、E、0。E代表Eden区,S代表 Survivor 区,H代表的是 Humongous(G1 用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的 N个Humongous 中),剩余的深蓝色代表的是0ld区,灰色的代表的是空闲的 region这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合GC模式。
G1 垃圾收集器工作流程

  • 初始标记( Initial Marking ):这个阶段仅仅只是标记GC Roots能直接关联到的对象,这阶段需要停顿线程,但是耗时很短。

  • 并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。

  • 最终标记( Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后遗留记录。

  • 筛选回收( Live Data counting and Evacuation ):负责更新Region 的统计数据,对各个Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个 Region 来构成会收集,然后把回收的那一部分 Region 中的存活对象==>复制==>到空的Region中,最后对那些 Region 进行清空
    G1 垃圾收集器的特点

  • 并行与并发: G1 能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU 或者CPU 核心)来缩短Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java线程执行的 GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行,

  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果

  • 空间整合: G1 从整体来看是基于"标记-整理"算法实现的收集器,从同部(两个 Region 之间)上来看是基于"标记-复制算法实现的。这意味着 G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

  • 用户指定期望停顿:允许用户指定期望的停顿时间是 G1收集器很强大的一个功能,设置不同的期望停顿时间,可以让 G1 在不同的场景下取得吞吐量和延迟之间的最佳平衡。G1 的默认停顿目标为 200 秒,一般来说,设置为一百毫秒至两百毫秒这个区间都很正常。如果期望停顿时间设置过短,会导致由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器的收集速度会跟不上分配速度,导致垃圾慢慢堆积。
    G1垃圾收集器与CMS垃圾收集器的区别
    算法不同 : CMS 采用"标记-清除 "容易产生内存碎片,执行若干次 GC 后进行 1 次碎片整理。 G1 从整体来看是基于"标记-整理 "算法实现的收集器,从同部(两个 Region 之间)上来看是基于"标记-复制 "算法实现。意味着G1垃圾收集器不会产生内存空间碎片,垃圾收集完成后能提供规整的可用内存,不会导致因为大对象分配内存时无法找到连续内存空间而提前触发垃圾收集。
    场景不同 :小内存应用上CMS 的表现大概率优于 G1,而在大内存应用中, G1 则能发挥优
    势。大小内存的参考值分水岭大概在 6GB-8GB。

相关推荐
公贵买其鹿14 分钟前
List深拷贝后,数据还是被串改
java
tinker在coding19 分钟前
Coding Caprice - Linked-List 1
算法·leetcode
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭5 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
XH华5 小时前
初识C语言之二维数组(下)
c语言·算法
暮湫5 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java