【JVM篇2】垃圾回收机制

news2024/12/24 3:49:01

目录

 一、GC的作用

申请变量的时机&销毁变量的时机 

内存泄漏

垃圾回收的劣势

二、GC的工作过程

回收垃圾的过程

第一阶段:找垃圾/判定垃圾

      方案1:基于引用计数(非Java语言)

引用计数方式的缺陷

方案2:可达性分析(基于Java语言)

GCRoots是哪些变量(3类)

第二阶段:回收垃圾(释放内存)

策略1:标记-清除策略 

      策略1存在问题分析:(内存碎片)

策略2:复制算法

复制算法存在问题分析: 

策略3:标记——整理

分代回收

三、垃圾收集器有哪些

第一类:Serial收集器&Serial Old收集器(串行收集)

第二类:ParNew收集器&Parallel Old收集器&Parallel Scavenge收集器(并发收集)

第三类:CMS收集器

步骤1:找到GCRoots(会引发STW)

步骤2:并发标记

步骤3:重新标记(会引发STW)

步骤4:回收内存

第四类:G1收集器


一、GC的作用

       GC:全称是Garbage Clean(垃圾回收)。我们平时写代码的时候,经常会申请内存。例如:

创建变量、new对象、加载类...

       但是,由于内存空间是有限的,因此就需要"有借有还"。

       如下代码,就是申请了两个变量:一个a,另外一个是object

int a=3;

Object obj=new Object()

申请变量的时机&销毁变量的时机 

        申请一个变量(申请内存的时机)是确定的。就是new或者int a=...这种。但是,这个变量什么时候不需要使用了,那这个时期就不确定了

       例如:内存释放得偏早:如果还想要使用obj对象,但是如果这一个对象被回收了,那这样就显得不合理了。       

       又或者:内存的释放比较偏迟,对象一直占着"坑位"。

对于内存什么时候被释放这个问题, 不同的语言有不同的处理方式。

       对于C语言:程序没有提供垃圾回收机制。因此当内存需要释放的时候,必须由程序员手动进行释放(调用free函数),因此,就会引入一个臭名昭著的问题,那就是"内存泄漏"。


内存泄漏

        如果申请的内存越来越多,那么就意味着可用的内存越来越少,最终无内存可用了。这种现象就叫做"内存泄漏"。

       虽然垃圾回收可以让开发的程序员专注于设计业务上面的代码,无需关心内存泄露的问题,但是仍然有一定的劣势


垃圾回收的劣势

     1、引入了额外的开销(消耗资源更多了)

     2、可能会影响程序的流畅运行:垃圾回收经常会出现:STW(stop the work)问题。


二、GC的工作过程

回收的是什么样的对象

在上一篇文章当中,我们提到了:JVM的内存区域划分主要分为4个部分:

程序计数器、栈、堆、方法区。

对于栈区,只要方法返回之后,就会自动从栈上面消失了,不需要GC。

对于堆区,就很需要GC了,因为堆区当中存放的大量都是new出来的对象。

我们来画一张图,描述一下根据内存使用与否的图:

因此,需要回收的对象,都是一些没有使用,但是同时也占用着内存的对象。


回收垃圾的过程

垃圾回收的过程,分为两大阶段:

第一阶段:找垃圾/判定垃圾

      方案1:基于引用计数(非Java语言)

       针对每一个对象,都引入一小块的内存,保存这一个对象有多少个引用指向它。

       例如:(此时有两个引用都指向new Test()对象)

//t1引用指向new Test()对象
Test t1=new Test();
//t2引用指向new Test()对象
Test t2=t1;

        那么,此时在new Test()当中,就会有一个引用计数器,显示指向这个对象的引用个数为2。

        那么,当引用计数为0的时候,也就意味着此时没有引用指向这个对象了,需要GC对于这一个对象进行回收操作。

        什么时候引用计数为0呢?下面举一个例子:

 private static void func2() {
        //让t1指向new Test1()对象
        Test1 t1=new Test1();
        //让t2指向new Test1()对象
        Test1 t2=t1;
    }

        在一个方法当中,两个引用(t1,t2)同时指向了new Test1()对象。 当调用func2()方法的时候,t1和t2引用会保存在func2()方法的栈帧上面。两个引用同时指向了堆上面的new Test1()对象。  

       当func2()调用结束之后,会从栈帧上面消失,那么t1和t2引用也会随之消失。

       那么,也就意味着:new Test2()这一个对象没有引用指向它了,认为它是一个"垃圾",也就会被回收。        


引用计数方式的缺陷

缺点1:空间利用率比较低

       每一个new的对象都必须要搭配一个计数器来记录几个引用。引用计数器的大小为4个字节,但是如果一个对象除了引用计数器以外的部分本身也就只有4个字节大小,那么就意味着比较浪费空间。

        

缺点2:会有循环引用的问题

下面,来举一个例子说明一下什么是循环引用问题:

首先:创建一个Test类,在内部有一个属性,就是Test t=null;

然后:在测试类当中,创建这一个类的实例对象:


class Test {

    Test t = null;
}

/**
 * @author 25043
 */
public class Test2 {


    public static void main(String[] args) {
        Test t1 = new Test();

        Test t2 = new Test();
    }
}

到这一步的时候,来画一个引用——对象的指向图:

然后,接下来:执行下面的代码:

 public static void main(String[] args) {
                  //对象1
        Test t1 = new Test();
                  //对象2
        Test t2 = new Test();

        t1.t = t2;

        t2.t = t1;
    }

       到了这一步,再画一下引用指向的图:(把t2引用指向的对象赋给了对象1的t属性把t1引用指向的对象赋给了对象2的t属性)

       

到这一步的时候:

对象1有两个引用指向(t1t2.t);

对象2有两个引用指向(t2t1.t)。

 接下来:令:t1=null,t2=null。

public static void main(String[] args) {
                //对象1
        Test t1 = new Test();
                //对象2
        Test t2 = new Test();

        t1.t = t2;

        t2.t = t1;

        t1 = null;

        t2 = null;
    }

那么此时可以认为:

t1的指向为null,并且t2的指向为null。

那么对应的指向对象1的引用减少了一个,只剩下(t2.t)

同时,指向对象2的引用也减少了一个,只剩下(t1.t)

两个对象的引用计数器各自减少为1

       由于引用计数不为0,也就是两个对象互相引用。那么,这两个对象无法被回收。但是,外部的引用又无法访问这两个对象。因此,这两个对象就永远无法被回收,也永远无法被使用。这样,也就出现了内存泄漏。

由于上述的两个缺点,因此引入了方案2(基于Java语言的解决方案:基于可达性分析)


方案2:可达性分析(基于Java语言)

       通过一个额外的线程,定期地针对整个内存空间的对象进行扫描。

       有一些起始的位置(称为GCroots),然后类似于深度优先搜索的方式,把可以访问到的对象都标记一遍。那么,带有标记的对象就是"可达的"。没有被标记的对象,那就是"垃圾"。这样就很好地解决了对象不可达的问题。(避免了两个对象相互引用、但是没有外部引用指向的问题)    

       尽管可达性分析方法可以有效解决引用循环的问题,但是如果一个程序当中的对象特别多,那么也一定会造成比较大的性能损耗,因为整个搜索的过程也是比较消耗时间的。


GCRoots是哪些变量(3类)

        第一类上的局部变量;

        第二类常量池当中的引用指向的变量;

        第三类方法区当中的静态成员指向的对象。  


第二阶段:回收垃圾(释放内存)

      回收垃圾主要分为三种策略:

      策略1:标记-清除策略;

      策略2:复制算法;

      策略3:标记-整理策略

下面,将分别介绍这三种策略:


策略1:标记-清除策略 

       标记,就是可达性分析的过程。例如在一次搜索当中,发现了以下几个部分是"垃圾"。清除,就是直接释放内存。

      策略1存在问题分析:(内存碎片)

       此时如果直接释放,虽然内存的确还给了操作系统了,但是内存还是离散的,也就不是连续的,这样带来的问题就是"内存碎片",影响程序的运行效率。


策略2:复制算法

为了解决内存碎片,引入的复制算法。如下图:把内存一分为二

然后,把正常的对象(没有被标记为垃圾的对象)的拷贝到令一半。

最后,把左侧的空前全部释放掉。此时,内存碎片问题就迎刃而解了。

复制算法存在问题分析: 

复制算法有效解决了上述的内存碎片问题,但是,仍然有以下的两个问题没有解决:

问题1:内存空间利用率低,只能利用一半的空间。

问题2:开销大。如果垃圾比较少,那么这种搬运得不偿失


策略3:标记——整理

这个过程,就是把正常的对象(没有被标记为垃圾的)往前搬运。最后,释放掉最后面的内存。

下图当中,灰色部分的为垃圾。

       但是,这个拷贝也是有开销的。

       上述的3种方案,虽然可以解决问题,但是都有缺陷。因此,实际上JVM当中,会结合多种方案一起来实现,并不是采用单一的策略。这种方式,就是"分代回收"。


分代回收

       分代回收,其实就是针对对象进行"分类",根据对象的"年龄"进行回收。

       对象的年龄:每熬过GC的一轮扫描没有被回收,那么对象的年龄就+1岁。这个年龄存储在"对象头"当中。


       大致是这样的一个过程:

       存储对象的内存区域大致就被分为了两部分:新生代和老年代

       在新生代当中,分为了两部分:伊甸区和幸存区。一共有2个幸存区

       步骤1:对于刚刚产生的对象,都会被存放在"伊甸区"

       步骤2:如果熬过一轮GC,那么就会被拷贝到"幸存区",(应用了复制算法)。但是大部分对象都熬不过一轮的GC。

        步骤3:在后续的几轮GC当中,幸存区的对象就在两个幸存区当中来回拷贝。此处也是采用了"复制算法",来淘汰掉一些对象。

       步骤4:经过了多轮的GC后,如果一个对象还是没有被淘汰,那么就会被放入"老年代"。此时就认为这个对象存活的可能性就比较大了。对于老年代的对象来说,GC扫描的次数就远远低于新生代了。同时,老年代当中采用的就是"标记——整理"的方式来回收。

       但是,有一种特殊的情况,就是当一个对象特别"大",也就是占用内存比较多的时候,无需经过多轮GC的扫描,就可以直接进入"老年代"了,因为回收这一类的对象比较消耗性能


三、垃圾收集器有哪些

第一类:Serial收集器&Serial Old收集器(串行收集)

       这两个垃圾收集器是串行收集的。那么也就意味着,在垃圾的扫描和释放的时候,其他的业务线程都需要停止工作。这种方式扫描得慢、释放得慢、也产生了严重的STW。


第二类:ParNew收集器&Parallel Old收集器&Parallel Scavenge收集器(并发收集)

       这三个收集器、引入了多线程的方式来进行回收,也就是"并发收集"。并不影响业务线程执行业务代码。


第三类:CMS收集器

执行步骤:

步骤1:找到GCRoots(会引发STW)

找到GCRoots,但是会引起短暂的STW。


步骤2:并发标记

和业务线程一起执行。


步骤3:重新标记(会引发STW)


步骤4:回收内存

这个步骤,也是和业务线程一起执行的。


第四类:G1收集器

把整个内存,分成了很多个小的区域(Region)

给这些Region进行了不同的标记。有一些region存放新生的对象,有一些存放老年代的对象。

然后一次扫若干个Region(但不是全部扫完)

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯嵌入式ADC与DAC(都不需要中断)

目录 1.原理图 (1)ADC的原理图 (2)DAC的原理图 2.STM32CubeMX的配置 (1)ADC的配置 (2)DAC配置 3.代码部分 (1)ADC代码 (2)DA…

【C语言深度剖析】关键字(全)

文章目录一.存储类型关键字前言补充1:内存思考:补充2:变量与内存的关系补充3:变量的分类补充4:存储类补充5:删除数据是怎么删除的?1.auto2.register3.static4.extern基本用法:基本功能5.typedef…

「计算机组成原理」数据的表示和运算(二)

文章目录五、奇偶校验码六、算术逻辑单元ALU6.1 电路的基本原理6.2 加法器的设计6.2.1 一位全加器6.2.2 串行加法器6.2.3 串行进位的并行加法器6.2.4 并行进位的并行加法器七、补码加减运算器八、标志位的生成九、定点数的移位运算9.1 算数移位9.2 逻辑移位9.3 循环移位五、奇偶…

Matlab生成sinc信号

Matlab生成sinc信号 在Matlab中生成sinc信号非常容易。首先,我们需要了解什么是sinc波形。 sinc波形是一种理想的信号,它在时域上是一个宽度为无穷的矩形函数,而在频域上则是一个平的频谱。它的公式为: sinc⁡(x)sin⁡(πx)πx\…

YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py

前言 本篇文章主要是对YOLOv5项目的训练部分train.py。通常这个文件主要是用来读取用户自己的数据集,加载模型并训练。 文章代码逐行手打注释,每个模块都有对应讲解,一文帮你梳理整个代码逻辑! 友情提示:全文近5万字…

学习电气自动化PLC编程最基础的十大知识点详解

这篇文章其实是学习PLC自动化过程中必须要理解的基础问题,不管是西门子PLC还是三菱PLC,抑或欧姆龙PLC,以及国产品牌的PLC,这些问题都必须理解透,才能更好的开始自动化编程。不然指令学完了梯形图的逻辑可能还是搞不懂&…

Datatables展示数据(表格合并、日期计算、异步加载数据、分页显示、筛选过滤)

系列文章目录 datatable 自定义筛选按钮的解决方案Echarts实战案例代码(21):front-endPage的CJJTable前端分页插件ajax分页异步加载数据的解决方案 文章目录系列文章目录前言一、html容器构建1.操作按钮2.表格构建二、时间日期计算三、dataTables属性配置1.调用2.过…

java多线程与线程池-03线程池与阻塞队列

第6章 线程池与阻塞队列 6.1 Queue接口 队列是一种特殊的集合,一般队列都具有先进先出(FIFO)的特性(并不绝对要求)。优先级队列(PriorityQueue)按照元素的比较方法排序,其他队列基本采用自然序排队。 队列Queue接口实现了Collection接口,offer()方法负责把元素插入…

带头双向循环链表及链表总结

1、链表种类大全 1、链表严格来说可能用2*2*28种结构,从是否带头,是否循环,是否双向三个角度区分。 2、无头单向循环链表一般不会在实际运用中直接存储数据,而会作为某些更复杂结构的一个子结构,毕竟它只在头插、头删…

【数据结构之二叉树】——二叉树的概念及结构,特殊的二叉树和二叉树性质

文章目录一、二叉树的概念及结构1.概念2.现实中的二叉树3. 特殊的二叉树:3.二叉树的性质二、二叉树练习题总结一、二叉树的概念及结构 1.概念 一棵二叉树是结点的一个有限集合,该集合: 或者为空由一个根节点加上两棵别称为左子树和右子树的二叉树组成…

线性表的链式表示

文章目录1.单链表1.1单链表的表示1.1.1构建 带头结点的单链表1.2基本操作1.2.1 头插法1.2.2 尾插法1.2.3 按序号查找结点1.2.4 按值查找表结点1.2.5 插入结点操作扩展:前插操作1.2.6 删除结点操作扩展:删除结点*p1.2.7 求表长操作2.双链表2.1 双链表的表…

JVM相关知识

JVM类加载过程类什么时候被加载什么情况下会发生栈内存溢出JVM内存模型常量池回收方法区垃圾回收流程圾收集算法分代收集理论标记-清除算法标记-复制算法标记-整理算法类加载过程 加载–验证–准备–解析–初始化–使用–卸载 ​ 加载:通过全类名获取类的二进制流…

【C++】非类型的模板参数,特化

目录 1.类型模板参数和非类型模板参数 2.特化 3. 模板的分离编译 4.模板的优缺点 1.类型模板参数和非类型模板参数 之前写模板传的都是类型——类型模板参数 现在想定义两个静态数组,数组长度不同,就可以用模板参数传数值而不是传类型 非类型模板…

Docker与微服务实战2022

基础篇(零基小白)1.Docker简介1.1 是什么问题:为什么会有docker出现?您要如何确保应用能够在这些环境中运行和通过质量检测?并且在部署过程中不出现令人头疼的版本、配置问题,也无需重新编写代码和进行故障修复? 答案就…

Android源码分析 - View的触摸事件分发

0. 相关分享 Android源码分析 - InputManagerService与触摸事件 1. 接收Input系统发送来的事件 时序图源:稀土掘金 在注册Window的时候,来到ViewRootImpl,其中不仅发起窗口注册,还开启了输入事件的监听: //ViewRoo…

nuxt3使用总结

目录 背景 安装 项目配置 路由 Tailwindcss引入 全局样式配置 css预处理器 安装 Tailwindcss 项目的配置 部署上线 seo优化 背景 新入职了一家公司,刚进入公司第一个需求就是先做一个公司的官网,需要使用vue写,作为祖师爷的粉丝…

Java 电话号码的组合

电话号码的字母组合中等给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。示例 1:输入:digits "23…

案例学习--016 消息队列作用和意义

简介MQ全称为Message Queue, 是一种分布式应用程序的的通信方法,它是消费-生产者模型的一个典型的代表,producer往消息队列中不断写入消息,而另一端consumer则可以读取或者订阅队列中的消息。主要产品有:ActiveMQ、RocketMQ、Rabb…

【RV1126】RKMedia模块简介

文章目录简介源码与编译rkmedia log等级配置目录参考文档:【Rockchip RKMedia Development Guide】rkmedia的手册在sdk目录下/docs/RV1126_RV1109/Multimedia rkmedia的代码在sdk目录下/external/rkmedia rkmedia的demo在sdk目录下/external/rkmedia/examples&…

antlr4-maven-plugin简单学习

1. 序言 antlr4-maven-plugin的官方介绍为: The ANTLR 4 plugin for Maven can generate parsers for any number of grammars in your project.博客《 mac上的Antlr4环境搭建》,有介绍如何通过antlr4-maven-plugin实现.g4文件的编译 这里将介绍antlr4-…