JVM基础(5)——JVM垃圾回收算法

news2024/12/26 10:45:32

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

一、简介

我们在前两章中,已经讲解了JVM垃圾回收的基本流程和对象存活判定的算法,但是,并没有深入垃圾回收内部的细节。本章,我们就深入垃圾回收的内部,看看JVM到底是如何进行对象内存的回收的。

二、复制算法

复制算法,主要用于 新生代 中对象的回收。其基本思路就是:将新生代内存按划分为大小相等的两块,每次只使用其中的一块,当一块内存用完了, 将存活的对象移动到另外一块上面 ,然后在把已使用过的内存空间一次清理掉。

2.1 算法流程

我们以示例代码来看复制算法的执行流程:

    public class Kafka {
    
        public static void main(String[] args) throws InterruptedException {
            loadReplicaFromDisk();
        }
    
        private static void loadReplicaFromDisk() {
            ReplicaManager replicaManager = new ReplicaManager();
            replicaManager.load();
        }
    }

假设程序执行到replicaManager.load(),JVM的内存数据结构如下,其中“大量垃圾对象无人引用”表示其它程序产生的垃圾对象,此时新生代中用于分配对象内存的区域也快满了,再次为对象分配内存时就会触发“Minor GC”:

此时,一种最基本的思路就是,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。但是这种方法问题也很明显: 产生大量内存碎片,导致后续需要为大对象分配空间时内不足 。所以,JVM很少用这种方法,而是先将存活对象转移到另一块区域:

然后一次性把原来那块内存清空:

上述整个流程,就是所谓的复制算法: 把新生代划分为两块内存区域,只使用其中一块,当这块快满的时候,把存活对象一次性转移到另一块,保证没有内存碎片,然后清空原来那块,依次循环往复 。

2.2 算法优化

上述复制算法的缺点很明显:即 对内存的使用效率太低 。比如我们给新生代分配了1G内存,那其实只有512MB是实际使用的,很浪费内存空间。那么如何来优化呢?

我们回顾下 新生代中对象的特点:朝生暮死,也就是说新生代的绝大多数对象在经历1次GC后就会被回收掉,存活率非常低。

根据这个特点, HotSpot VM 采用了一种做法,把新生代区域划分成了三块: 1个Eden区(80%),2个Survivor区(各占10%) ,最开始,对象只在Eden进行分配:

如果Eden区快满了,此时触发GC会将Eden区中的存活对象转移到其中一块Survivor中,同时清空Eden:

下一次再分配空间时,依然在Eden区分配,然后触发GC,将Eden的存活对象和上一次使用的Survivor中的存活对象转移到另一块空白Survivor中,然后清空Eden和使用过的Survivor,循环往复。

这种内存划分方式的最大好处就是只有10%的空间是闲置的,无论是垃圾回收的性能、内存碎片的控制、内存使用率,都非常好。

三、标记整理算法

通过前一节,大家应该已经了解了新生代的垃圾回收算法,本节我们就来看下老年代的垃圾回收算法——标记整理算法。

3.1 何时进入老年代

这里有一个问题,新生代的对象什么时候会进入老年代?先给出一个结论,一共有五种情况:

  • 新生代对象的年龄超过一定阈值(默认15);
  • 动态年龄判断
  • 大对象直接分配
  • Survivor区空间不足

年龄阈值

之前我们提到过,新生代中的对象每逃过一轮GC,年龄都会加1,到年龄达到15时(也可以通过JVM参数-XX:MaxTenuring Threshold设置),就会被转移到老年代:

动态年龄判断

动态对象年龄判断的规则是: Survivor区的存活对象年龄从小到大进行累加,当累加到 X 年龄时的总和大于 Survivor区空间的50%时,那么比X大的对象都会晋升到老年代。

举个例子,比如当前Survivor区的分布如下,累加结果45%小于50%:

此时新生代GC后,有6%的对象进入Survivor区,则Survivor区分布如下图:

这时从1岁加到4岁的对象总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升 。此时再经过一次新生代GC后,又有40%的对象进入Survivor区,Survivor区分布如下图:


Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代。

可以使用-XX:TargetSurvivorRatio来设置Survivor区空间的百分比,默认值是50

大对象

对于一些大对象,JVM会直接将其分配到老年代。通过参数-XX:PretenureSizeThrehold,可以设置阈值,单位为字节。

JVM之所以要这么做,是为了避免新生代中出现屡次逃过GC的大对象,大对象在新生代的Eden和Survivor区的来回复制开销比较大。

Survivor区空间不足

最后一种情况就是,Minor GC之后发现存活对象太多,没法放入另一块Survivor区域中,比如下面这种情况:

这时候就必须把这些对象全部迁移到老年代去:

3.2 空间分配担保

前面讨论了新生代的存活对象何时会转移到老年代,那么问题又来了,如果老年代区域的内存空间不足了怎么办?这里就涉及了 空间分配担保机制 。

所谓空间分配担保,指在执行任何一次Minor GC之前,JVM会检查 老年代的最大连续可用空间 是否大于 新生代所有对象的总大小 :

如果大于,说明这次Minor GC肯定是安全的,因为老年代可以容纳新生代中的所有对象;

如果小于,则 JVM 会查看-XX:HandlePromotionFailure参数值,这个参数值表示是否允许担保失败:

  • 如果允许(HandlePromotionFailure==true),则看下 老年代的最大连续可用空间 是否大于 历次Minor GC后进入老年代的对象平均大小 。如果大于,就进行minior GC,如果这次Minior GC失败了,就会进行FULL GC(所谓FULL GC,就是既对老年代进行垃圾回收,也对新生代进行垃圾回收);如果小于,先进行FULL GC,再Minor GC。
  • 如果不允许(HandlePromotionFailure==false),则直接触发FULL GC,然后再进行一次Minor GC。

如果经过上面的操作,老年代可用空间最后发现还是不够,就会导致所谓的OOM内存溢出了。

总之,空间分配担保机制的核心目的就是 避免频繁FULL GC,能先预判就先预判 ,实在不行才FULL GC,因为FULL GC的开销非常大,既要对老年代进行回收,也要对新生代进行回收。

3.3 算法流程

了解了新生代对象何时进入老年代,以及FULL GC的触发时机,我们就可以来看下老年代的 标记整理算法 的流程了。标记整理算法,其实就是先标记存活对象,然后将存活对象都向内存端边界移动,然后清理掉端边界以外的内存,这样就可以避免出现大量内存碎片。

我们通过示例来看下,假设JVM当前的内存状态如下,老年代中散落着各种存活对象:

接着,会将存活对象都往内存的一边移动,让它们尽量紧凑,然后一次性把垃圾对象清理掉:

四、线上示例

通过前两节的讲解,相信读者已经对新生代的复制算法、老年代的标记整理算法有所了解,本节我们将通过一个生产系统的GC案例,让大家更加透彻的理解JVM中如果进行对象分配和老年代转移,以及Minor GC和Full GC的全过程。

4.1 背景

假设现在生产环境有一套“数据计算系统”,不停地从MySQL等各类数据源提取数据到内存中进行计算,系统是分布式的。每个节点(机器)每分钟执行100次操作(提取数据并计算,每次操作耗时10s),每次操作1万条数据,每条数据大小为1KB左右,那么每次数据的总大小就是10MB:

每台机器的配置是4核8G,JVM分配4G内存,其中新生代1.5G,老年代1.5G。

整个系统的初始背景大致就是上面这样,下面来分析可能存在的各种问题。

4.2 频繁Full GC

我们先来看下新生代的空间什么时候会被占满,按照8:1:1来分配Eden和Survivor区,如下图:

每执行一次操作,Eden区就会填充10MB数据,一分钟执行100次操作就是1000MB,所以 Eden区基本上1分钟左右就会被占满 。再执行操作时,就会进行Minor以回收一部分的垃圾对象:

首先,检查老年代的连续可用内存空间是否足够(即大于新生代中的所有存活对象大小),如下图,老年代目前是空的,1.5G的可用内存空间可以容纳Eden区中的1.2G对象,所以会直接进行Minor GC:

那么, 此时Eden区中有多少对象还是存活的呢? 之前说了每次操作耗时10s,那么在1分钟内的最后10s时,前面0-50s的任务已经执行完了,1分钟操作100个任务,所以大约有1/6的任务还没有执行完毕,即大约还有20个任务在计算中(大约200MB对象存活):

其实线上一般是通过GC日志去分析存活对象的大小的,GC日志中清楚的记录了每次Minor GC进入到老年代的对象大小(后面我们会详细讲解如何看懂GC日志),根据我们的线上日志分析,大约也还有200MB对象是存活的。

注意,每一块Survivor区的大小只有100MB,所以是无法容纳200MB的存活对象的,所以会通过空间担保机制,转移到老年代中,并清空Eden区,此时JVM内存空间结构如下:

由于每分钟老年代都被填充200MB存活对象,所以到第3分钟结束时,老年代已经有400MB空间被占满,且Eden区也被占满,此时如果要进行Minor GC,会怎么样呢?

首先,依然检查老年代的连续可用内存空间是否足够(即大于新生代中的所有存活对象大小),此时发现空间是不够的,老年代只有1.1GB可用,而新生代的所有对象大小有1.2GB。

此时,就会判断是否开启了空间担保机制——即判断HandlePromotionFailure是否为true,如果开启了(一般生产环境都会开启),就会看下历代晋升到老年代的对象大小是否小于老年代可用空间,根据之前的计算,历代晋升到老年代的对象大小约为200MB,小于1.1GB,所以JVM就会放心的进行一次Minor GC,此时又有200MB对象进入到老年代。

重复上述过程,大约经过8分钟,经历7次Minor GC后,JVM内存空间结构如下,此时老年代剩余可用空间大约100MB,Eden区已被占满:

此时又会进行Minor GC前的检查,但是老年代的可用空间已经比历代晋升到老年代的对象空间小了,所以会 直接触发一次Full GC ,将老年代中的垃圾对象回收(假设此时老年代中的对象全部都可回收):

然后紧接着再进行一次Minor GC,将Eden区中的200MB存活对象转移到老年代:

按照上述这个模型, 基本上8分钟左右就会触发一次Full GC ,这个频率对于生产环境是不可接受的,因为Full GC会严重影响系统性能,这个后面章节我们会详细讲解。

4.3 优化

那么该如何进行优化呢?最基本的思路就是 增加Survivor的内存大小 ,因为正是Survivor区不能容纳存活对象(200MB)导致必须晋升到老年代。所以重新分配新生代大小为2G,老年代为1G,同时改变Eden和Survivor的空间比例,这样Survivor区就能容纳每次Minor GC后的存活对象,如下图:

比如经过一段时间,JVM内存结果如下,Eden区被占满,Survivor1区有200MB上一轮Minor GC后的存活对象:

然后此时进行Minor GC,会先清理到S1区中的所有对象,然后将Eden区中的存活对象(200MB)转移到S2区:

这样,基本上就很少会有对象进入到老年代,Full GC的频率能降低到几小时一次。

五、总结

最后来总结下本章的内容,本章主要介绍了新生代的复制算法和老年代的标记整理算法的流程,重点需要掌握的是以下几点:

  1. 新生代对象何时会进入老年代?
  2. 何时会触发新生代的Minor GC?
  3. 何时会触发FULL GC?
  4. 空间分配担保机制的作用是什么?

同时,本章也给出了一个线上示例,帮助读者更好的理解JVM分代垃圾回收的整个流程。下一章开始,我们将详细介绍各种垃圾回收器,看看它们内部是如何运用GC算法进行垃圾回收的。

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

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

相关文章

【机器学习】模型调参工具:Hyperopt 使用指南

机器学习| 模型调参工具:Hyperopt 使用指南 前言1. Hyperopt是什么?2. Hyperopt的优缺点3. 如何使用 Hyperopt 进行调参3.1 安装 Hyperopt3.2 构建超参数空间3.3 定义目标函数3.4 运行 Hyperopt 优化3.5 获取最优超参数 4. XGB调参代码示例参考资料 前言…

Java Http各个请求类型详细介绍

1. 前言 在Spring Boot框架中,HTTP请求类型是构建Web应用程序的重要组成部分。常见的请求类型包括GET、POST、PUT和DELETE,每种类型都有其特定的用途和特点。本文将详细比较这四种请求类型,帮助您在开发过程中做出明智的选择。 2. GET请求…

12.扩展字典(ExtensionDictionary)

愿你出走半生,归来仍是少年! 环境:.NET FrameWork4.5、ObjectArx 2016 64bit、Entity Framework 6. 在10.扩展数据(XData)中我们讲到每个DbObject有一个XData对象可以存储数据,除此之外每个DbObject对象还有一个ExtensionDictionary(扩展字典)可以进行数据存储。…

vue3+ts+vite中封装axios,使用方法从0到1

一、安装axios npm install axios types/axios --save二、配置代理vite.config.ts,如果没有需要新建该文件 module.exports {server: {proxy: {/api: {target: http://localhost:5000, // 设置代理目标changeOrigin: true, // 是否改变请求源地址rewrite: (path)…

【开源】基于JAVA+Vue+SpringBoot的大病保险管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统配置维护2.2 系统参保管理2.3 大病保险管理2.4 大病登记管理2.5 保险审核管理 三、系统详细设计3.1 系统整体配置功能设计3.2 大病人员模块设计3.3 大病保险模块设计3.4 大病登记模块设计3.5 保险审核模块设计 四、…

写点东西《Docker入门(上)》

写点东西《Docker入门(上)》 环境变量 Docker 镜像 Docker CMD 与 ENTRYPOINT 有什么区别 Docker 中的网络: Docker 存储: Docker 是一个工具,允许开发人员将他们的应用程序及其所有依赖项打包到一个容器中。然后&…

SQL--case语句

case语句,按从上到下的书写顺序计算每个WHEN子句的布尔表达式。返回第一个取值为TRUE的布尔表达式所对应的结果表达式的值。如果没有取值为TRUE的布尔表达式,则当指定了ELSE子句时,返回ELSE子句中指定的结果;如果没有指定ELSE子句&#xff0c…

【Docker】快速入门之Docker的安装及使用

一、引言 1、什么是Docker Docker是一个开源的应用容器引擎,它让开发者可以将他们的应用及其依赖打包到一个可移植的镜像中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之…

1688采购工厂货源对接API

1688现有API列表 item_get 获得1688商品详情item_search 按关键字搜索商品item_search_img 按图搜索1688商品(拍立淘)item_search_suggest 获得搜索词推荐item_fee 获得商品快递费用seller_info 获得店铺详情item_search_shop 获得店铺的所有商品item_p…

解决python画图无法显示中文的问题

python画图遇到的问题: 中文不显示: 解决方法:把字体设置为支持中文的字体,比如黑体 黑体下载链接: 链接:https://pan.baidu.com/s/1BD7zQEBUfcIs6mC2CPYy6A?pwdv120 提取码:v120 pyhon…

SpringBoot默认配置文件

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringBoot ✨特色专栏: MySQL学习 🥭本文内容:SpringBoot默认配置文件 📚个人知识库: Leo知识库,欢迎大家访问 1.前言☕…

Git 基础指令

Git 基础指令 本章涵盖了我们在使用 Git 完成各种操作时将会用到的各种基本命令。 在学习完本章之后,我们应该能够配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)…

教你如何打造自己的知识付费平台!

明理信息科技知识付费saas租户平台 一、确定目标群体 首先,你需要明确你的知识付费平台的目标用户是谁。这将帮助你确定所需的内容和功能,以及如何吸引和留住这些用户。例如,如果你的目标群体是职场新人,你的平台可能需要提供职…

如何编写和管理自动化测试用例

开始本篇文章之前,先来介绍下什么是自动化测试用例,即通过编写脚本程序来模拟用户操作和功能验证,并由机器自动执行无人值守的测试用例。 相比手工测试用例,自动化测试用例更快、更准确、更可靠、容易重复执行,且每次…

【python】python新年烟花代码【附源码】

欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 新年的钟声即将敲响,为了庆祝这个喜庆的时刻,我们可以用 Python 编写一个炫彩夺目的烟花盛典。本文将详细介绍如何使用 Pygame 库创建一个令人惊叹的烟花效果。 一、效果图: 二…

植物大战僵尸-C语言搭建童年游戏(easyx)

游戏索引 游戏名称&#xff1a;植物大战僵尸 游戏介绍&#xff1a; 本游戏是在B站博主<程序员Rock>的视频指导下完成 想学的更详细的小伙伴可以移步到<程序员Rock>视频 语言项目&#xff1a;完整版植物大战僵尸&#xff01;可能是B站最好的植物大战僵尸教程了&…

程序员有哪些接单的渠道?

这题我会&#xff01;程序员接单的渠道那可太多了&#xff0c;想要接到合适的单子&#xff0c;筛选一个合适的平台很重要。如果你也在寻找一个合适的接单渠道&#xff0c;可以参考以下这些方向。 首先&#xff0c;程序员要对接单有一个基本的概念&#xff1a;接单渠道可以先粗略…

034 - STM32学习笔记 - TIM定时器(三) - 高级定时器2

034 - STM32学习笔记 - TIM定时器&#xff08;三&#xff09; - 高级定时器2 哥们最近搞了个公众号&#xff0c;后面的文章会同步在公众号上发布&#xff0c;各位看官帮忙点点关注&#xff0c;后续一些其他方面的学习内容也会在公众号上发布&#xff0c;有兴趣的可以看看哟&…

电脑可以连接网络但浏览器无法访问部分或全部网页

啾咪&#xff01;离大谱了&#xff0c;电脑一段时间没有用&#xff0c;最近打开却发现可以连接网络但是无法访问部分网页&#xff08;如CSDN&#xff09;&#xff0c;显示如下&#xff1a; 有三种解决方法&#xff1a; &#xff08;1&#xff09;清除DNS缓存 步骤&#xff1a;…

为什么删掉MySQL表中一半的数据,表文件大小却不变?

一个InnoDB表包含两部分&#xff1a;表结构定义和数据。表结构定义占用空间很小&#xff0c;所以主要来看一下表数据。 表数据既可以存放在共享表空间里&#xff0c;也可以是单独的文件。由参数innodb_file_per_table控制&#xff0c;这个参数值为OFF&#xff0c;则表示存放在…