两篇文章带你搞懂GC垃圾回收之基础篇

2024-01-03 16:38

本文主要是介绍两篇文章带你搞懂GC垃圾回收之基础篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1.JVM GC回收哪个区域的垃圾?
  • 2.判断对象可以回收的方法
    • 2.1 引用计数法
    • 2.2 可达分析算法
    • 2.3 什么对象可以当作GC Roots?
      • 虚拟机栈中的引用对象
      • 全局的静态的对象
      • 常量引用
      • 本地方法栈中JNI引用的对象
  • 3.垃圾回收算法
    • 3.1 标记清除算法
    • 3.2 复制算法
    • 3.3 标记压缩算法
    • 3.4 分代回收算法
  • 4.垃圾回收器
    • 理解什么是STW?
    • 4.1 Serial 和 Serial Old 回收器
    • 4.2 ParNew 回收器
    • 4.3 Parallel Scavenge 回收器
    • 4.4 Parallel Old 回收器
    • 4.5 CMS 回收器(重点)
      • 4.5.1 并发标记出现的问题:漏标和错标
      • 4.5.2 并发标记问题的解决:三色标记算法
      • 4.5.3 CMS巨大bug
    • 4.6 G1 回收器


1.JVM GC回收哪个区域的垃圾?

JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

2.判断对象可以回收的方法

2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

它的优点是简单、高效,但是缺点也是异常明显:这个方法无法解决对象循环引用的问题

    // 对象循环引用示例Object objectA = new Object();Object objectB = new Object();objectA.instance = objectB;objectB.instance = objectA;objectA = null;objectB = null;

假设我们有上面的代码。程序启动后,objectAobjectB两个对象被创建并在堆中分配内存,它们都相互持有对方的引用,但是除了它们相互持有的引用之外,再无别的引用。而实际上,引用已经被置空,这两个对象不可能再被访问了,但是因为它们相互引用着对方导致它们的引用计数都不为0,因此引用计数算法无法通知GC回收它们,造成了内存的浪费。如下图:对象之间的引用形成一个有环图。

img

2.2 可达分析算法

基于引用计数法无法回收循环应用,我们就有了一种新的方法。

可达分析算法,或叫根搜索算法,在主流的JVM中,都是使用的这种方法来判断对象是否存活的。这个算法的思路很简单,它把内存中的每一个对象都看作一个结点,然后定义了一些可以作为根结点的对象,我们称之为GC Roots。如果一个对象中有另一个对象的引用,那么就认这个对象有一条指向另一个对象的边。

image.png

像上面这张图,JVM会起一个线程从所有的GC Roots开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收

img

上图红色为无用的节点,可以被回收。

需要注意的是:基本所有的GC算法都引用根搜索算法这种概念。

2.3 什么对象可以当作GC Roots?

共有四种对象可以作为GC Roots

在这里插入图片描述

虚拟机栈中的引用对象

我们在程序中正常创建一个对象时,对象会在堆上开辟一块内存空间,同时会将这块空间的地址作为引用保存到虚拟机中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种对象可以作为GC Roots。

全局的静态的对象

也就是使用了static关键字定义了的对象,这种对象的引用保存在共有的方法区中,因为虚拟机栈是线程私有的,如果保存在栈里,就不叫全局了,很显然,这种对象是要作为GC Roots的。

常量引用

就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也作为GC Roots。

本地方法栈中JNI引用的对象

有时候单纯的java代码不能满足我们的需求,就可能需要调用C或C++代码(java本身就是用C和C++写的嘛),因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

3.垃圾回收算法

3.1 标记清除算法

img

  • 标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收,如上图。
  • 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片

3.2 复制算法

img

  • 复制算法将内存分为两个空间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空闲区间)则是空闲的。
  • 复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。
  • 复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。

3.3 标记压缩算法

img

  • 标记-压缩算法采用和 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。标记-清除 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

3.4 分代回收算法

在这里插入图片描述

  • 根据存活对象划分几块内存区域,一般是分为新生代老年代。然后根据各个年代的特点制定相应的回收算法。

img

年轻代复制算法的具体应用:

生成空间好比就是eden区,survivor分别是From幸存区、To幸存区,eden区会标记一些存活的对象拷贝到From区,然后清空eden区。

如果From区eden区都有垃圾,就把eden区From区都存活的对象全部拷贝到To区,然后清空eden区和From区

如果To区eden区都有垃圾,就把eden区To区都存活的对象全部拷贝到From区,然后清空eden区To区

反复循环,默认来回循环15次,如果活动的对象还是没有被垃圾回收器回收了,就存放到老年代

  • 新生代(new / Young)

    • 每次垃圾回收都有大量的对象死去,只有少量对象存活,选用复制算法比较合理。
    • 新生代回收可以称为:YGC
  • 老年代(old)

    • 老年代中对象的存活率较高,没有额外的空间分配对它进行担保,所以必须使用标记-清除或者标记-压缩算法进行垃圾回收。
    • 老年代回收可以称为:MGC
  • 整体回收可以称为:FGC

4.垃圾回收器

理解什么是STW?

  • 在垃圾回收时,都会产生应用程序的停顿,停顿产生时,整个应用程序会被卡死,没有任何响应。
  • java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他业务线程都被挂起(除了垃圾收集帮助器之外)。
  • Java中一种全局暂停现象,全局停顿所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于GC引起。
  • 没有一种垃圾回收器不会产生STW,就连CMS这种多线程并发执行的回收器也会产生STW
  • CMS产生STW的阶段刚好就在初始标记重新标记阶段

在这里插入图片描述

image-20201108142403450

说明:如果两个垃圾回收器之间存在连线说明他们之间是可以搭配使用的

4.1 Serial 和 Serial Old 回收器

  • Serial分为Serial、Serial Old,其中Serial工作在年轻代Serial Old工作在老年代
  • Serial是单线程的垃圾回收器,当垃圾回收线程开始的时候,业务线程必须暂停,直到垃圾回收线程结束
  • Serial使用的是复制算法,而Serial Old使用的是标记-压缩算法

image-20201108142143857

4.2 ParNew 回收器

  • ParNew可以认为是Serial的多线程版本
  • ParNew是多线程并行的,也就是说当多条垃圾回收线程并行工作时,此时的业务线程处于等待状态

image-20201108142428220

4.3 Parallel Scavenge 回收器

  • Parallel Scavenge 是一个年轻代的垃圾回收器,也就是说Parallel工作在年轻代
  • Parallel Scavenge是多线程并行的,也就是说当多条垃圾回收线程并行工作时,此时的业务线程处于等待状态
  • 采用复制算法实现
  • JDK1.8默认采用的垃圾回收器:Parallel Scavenge、Parallel Old

image-20201108144056102

4.4 Parallel Old 回收器

  • Parallel Old 是 Parallel Scavenge 的老年代版本,也就是说Parallel Old工作在老年代
  • Parallel Old是多线程并行的,也就是说当多条垃圾回收线程并行工作时,此时的业务线程处于等待状态
  • 采用标记-压缩算法

image-20201108144822801

4.5 CMS 回收器(重点)

  • CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
  • CMS是多线程并发的,也就是说垃圾线程业务进程是可以一起执行
  • 采用 标记-清除 算法实现。
  • 运行步骤:
    • 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
    • 并发标记(CMS concurrent mark):进行 GC Roots Tracing
    • 重新标记(CMS remark):修正并发标记期间的变动部分
      • 这里要注意:重新标记就不能业务线程和垃圾线程一起执行了,也就是不能并发执行了
    • 并发清除(CMS concurrent sweep)

image-20201108145740603

在这里插入图片描述

4.5.1 并发标记出现的问题:漏标和错标

  • 并发标记虽然说垃圾线程和业务线程一起执行,但是这种情况也会产生各种问题
  • 漏标:一个对象被GC标记为不是垃圾,但是随着业务的进行,该对象的引用消失了,就变成了垃圾,而这时垃圾回收并没有标记该对象为垃圾,这时候就会产生漏标的情况
    • 这就是著名的"浮动垃圾(floating garbage)"
    • 解决办法很简单,下次垃圾线程再循环的时候,该对象会被重新标记为垃圾,进行清理
  • 错标:一个对象被GC标记为垃圾,但是随着业务的进行,该对象被其他对象引用了,又变成不是垃圾了,而这时垃圾回收已经标记为该对象是垃圾了,这时候就会产生错标的情况

在这里插入图片描述

4.5.2 并发标记问题的解决:三色标记算法

漏标问题

  1. 某个状态下,黑色->灰色->白色

image-20201108161742712

  1. 如果一切顺利,不发生任何引用变化,GC线程顺着灰色的引用向下扫描,最后都变成黑色,都是存活对象

image-20201108171447455

  1. 但是如果出现了这样一个状况,在扫描到灰色的时候,还没有扫描到这个白色对象,此时,黑色对象引用了这个白色对象,而灰色对象指向了别人,或者干脆指向了null,也就是取消了对白色对象的引用

在这里插入图片描述

image-20201108162525835

  1. 那么我们会发现一个问题,根据三色标记规则,GC会认为,黑色对象是本身已经被扫描过,并且它所有指向的引用都已经被扫描过,所以不会再去扫描它有哪些引用指向了哪些对象
然后,灰色对象因为取消了对白色对象的引用,所以后面GC开始扫描所有灰色对象的引用时候,也不会再扫描到白色对象最后结果就是,白色对象直到本次标记扫描结束,也是白色,根据三色标记规则,认为它是垃圾,被清理掉但是实际情况,它明显是被引用的对象,是绝对不能当做垃圾来清除的,因为漏标,最后被当作垃圾清理掉了/*
漏标的两个充要条件:
1.有至少一个黑色对象在自己被标记之后指向了这个白色对象
2.所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
这两个条件,必须全满足,才会造成漏标问题.
*/
  1. CMS的解决方案就是重新标记:将A变成灰色,问题解决

在这里插入图片描述

4.5.3 CMS巨大bug

记住一句话:没有任何一个jdk版本的默认垃圾回收器是cms

在这里插入图片描述
image-20201108173529000

4.6 G1 回收器

  • G1是面向服务端的垃圾回收器。
  • 优点:并行与并发、分代收集、空间整合、可预测停顿。
  • 运作步骤:
    • 初始标记(Initial Marking)
    • 并发标记(Concurrent Marking)
    • 最终标记(Final Marking)
    • 筛选回收(Live Data Counting and Evacuation)

image-20201108173934277

这篇关于两篇文章带你搞懂GC垃圾回收之基础篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/566353

相关文章

离心萃取机废旧磷酸铁锂电池回收工艺流程

在废旧磷酸铁锂电池的回收工艺流程中,离心萃取机主要应用于萃取除杂的步骤,以提高回收过程中有价金属(如锂)的纯度。以下是结合离心萃取机应用的废旧磷酸铁锂电池回收工艺流程: 电池拆解与预处理 拆解:将废旧磷酸铁锂电池进行拆解,分离出电池壳、正负极片、隔膜等部分。破碎与筛分:将正负极片进行破碎处理,并通过筛分将不同粒径的物料分开,以便后续处理。 浸出与溶解 浸出:采用适当的浸出工艺(如二段式逆

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

C 语言基础之数组

文章目录 什么是数组数组变量的声明多维数组 什么是数组 数组,顾名思义,就是一组数。 假如班上有 30 个同学,让你编程统计每个人的分数,求最高分、最低分、平均分等。如果不知道数组,你只能这样写代码: int ZhangSan_score = 95;int LiSi_score = 90;......int LiuDong_score = 100;int Zhou

c++基础版

c++基础版 Windows环境搭建第一个C++程序c++程序运行原理注释常亮字面常亮符号常亮 变量数据类型整型实型常量类型确定char类型字符串布尔类型 控制台输入随机数产生枚举定义数组数组便利 指针基础野指针空指针指针运算动态内存分配 结构体结构体默认值结构体数组结构体指针结构体指针数组函数无返回值函数和void类型地址传递函数传递数组 引用函数引用传参返回指针的正确写法函数返回数组

【QT】基础入门学习

文章目录 浅析Qt应用程序的主函数使用qDebug()函数常用快捷键Qt 编码风格信号槽连接模型实现方案 信号和槽的工作机制Qt对象树机制 浅析Qt应用程序的主函数 #include "mywindow.h"#include <QApplication>// 程序的入口int main(int argc, char *argv[]){// argc是命令行参数个数,argv是