【JVM】JVM 垃圾收集器与内存分配策略

2023-11-17

JVM 垃圾收集器与内存分配策略

JVM内存区域可知Java运行时内存的各个区域。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,当方法结束或者线程结束时,内存就会跟着被回收了。

而只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,所以Java堆方法区这两个区域内存的分配和回收时动态的,垃圾收集器也只关注这部分内存的管理。


一.对象存活判断

1.引用计数算法

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

  • 优点:实现简单,效率高
  • 缺点:无法解决对象相互循环引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。
2.可达性分析算法

通过一系列的称为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的。如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

image.png

在Java, 可作为GC Roots的对象包括:

  • 在虚拟机栈中引用的对象
  • 在方法区中类静态属性引用的对象
  • 在方法区中常量引用的对象
  • 在本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用
  • 所有被同步锁(synchronized关键字)持有的对象

二.如何回收(垃圾收集算法)

分代收集理论:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过多次垃圾收集的对象就越难以消亡。

Java堆划分为新生代和老生代两个区域。在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

1.标记-清除算法

分为标记和清除两个阶段先标记出需要回收的对象(可达性分析算法或者引用计数算法),在标记完成后统一回收所有被标记的对象。

缺点:

  • 效率问题,标记和清除效率都不高。
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
    image.png
2.标记-复制算法

将可用内存划分为大小相等的两块,每次只使用其中的一块。当这块用完了,就将还存活的复制到另一块上,然后将已使用过的另一半内存一次性清除。

新生代中的对象98%都是朝生夕死的。将内存分为较大Eden和两个较小的survivor空间。每次使用Eden和其中一个survivor,回收时将存活的对象一次性地复制到另一块survivor中,再清理掉Eden和已用过的survivor。

HotSpot虚拟机Eden与Survivor默认的大小比例为8:1:1。即只让10%的新生代被浪费的,survivor空间不够时,需要依赖其他内存(老年代)进行分配担保,即让对象进入老年代。

优点:

  • 对整个半区进行回收,不会出现空间碎片。
  • 如果内存中多数对象都是可回收,就只需复制少数的存活对象。

缺点:

  • 如果内存中多数对象都是存活的,将产生大量的内存间复制的开销。
  • 可用内存缩减了一半,造成空间浪费。

image.png

3.标记-整理算法

复制在对象存活率较高时效率很低。根据老年代的特点提出该算法。标记过程同标记清除一样,但不是直接对可回收对象进行清理,而是让存活对象朝着一端移动,然后直接清理掉边界以外的内存。

标记-整理与标记-清除的差异在于整理是移动式的回收算法,清除是非移动的。

image.png

根据各年代特点分别采用最适当的GC算法。

在新生代中每次垃圾收集都能发现大批对象已死, 只有少量存活,因此选用标记-复制算法, 只需要复制少量存活对象就可以完成收集。

在老年代因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用**“标记—清理标记—整理”**算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。即:

  • 新生代:存活率低,使用复制算法
  • 老年代:存活率高,使用“标记-整理”或“标记-清除”算法

三.垃圾收集器

  • 新生代:Serial收集器  ParNew收集器  Parallel Scavenge收集器
  • 老年代:Serial Old收集器  Parallel Old收集器  CMS收集器

image.png

新生代:
1.Serial收集器

标记复制。单线程收集器,只会使用一个处理器或一条收集线程进行垃圾收集,而且在垃圾收集时,必须暂停其他所有工作线程,直到收集结束。优点:简单高效。

image.png

2.ParNew收集器

标记复制。是Serial收集器的多线程并行版本,同时使用多条线程进行垃圾收集,其余与Serial一样。目前唯一能与CMS收集器配合工作。

image.png

3.Parallel Scavenge收集器

标记复制。并行收集的多线程收集器。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而PS收集器的目的则是达到一个可控制的吞吐量。吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间))。

image.png

老年代:
4.Serial Old收集器

标记整理。Serial 收集器的老年代版本,单线程收集器。

5.Parallel Old收集器

标记整理。Parallel Old是Parallel Scavenge的老年代版本,多线程收集器。

6.CMS收集器(Concurrent Mark Sweep并发标记清除)

标记清除。

并发收集,回收停顿时间短。

步骤:

  1. 初始标记:停掉用户其他线程,仅标记GCRoots能直接关联到的对象,速度很快。
  2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,耗时长但不需要停顿用户线程,与垃圾收集线程一起并发执行。
  3. 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。耗时比初始标记长一点,但远低于并发标记。
  4. 并发清除:清理删除掉那些标记阶段判断为死亡的对象,因为标记清楚算法不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

总体而言,CMS收集器的内存回收过程是与用户线程一起并发执行的。

image.png

缺点:

  • CMS对处理器资源非常敏感。
  • CMS无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
  • CMS是标记清除,会产生大量碎片空间,对大对象内存分配带来麻烦。
7.G1收集器(Garbage First)

与其他收集器不同,G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新胜达的Eden空间、Survivor空间,或者老年代空间。收集器对不同角色的Region采用不同的策略去处理。优先处理回收价值收益最大的那些Region,也就是Garbage First的由来。

  • 从整体来看:“标记-整理” 算法
  • 从局部(两个Region之间)来看:“复制”算法

四.内存分配与回收策略

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

image.png

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的。

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,一般会伴随Minor GC 速度一般比Minor GC慢上10倍以上。

对象的内存分配从大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下也有可能直接分配在老年代中,分配的规则并非百分之百固定的,细节取决于当前使用的是哪一种垃圾收集器的组合,还有虚拟机中与内存相关的参数设置。

1.对象优先分配在Eden

大多数情况下,对象在新生代的Eden区分配,当Eden区没有足够空间时,虚拟机将发起一次Minor GC。

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

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

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳,将被移动到Survivor空间中,并且对象年龄为1.对象在Survivor区每“熬过”一次Minor GC,年龄就增加一岁,当年龄增加到一定程度(默认为15岁),就将被晋升到老年代中。对象晋升老年代年龄的阈值,可以通过参数设置。

4.动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到要求的年龄

5.空间分配担保

发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立,那么Minor GC确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管是有风险的,如果小于或者设置不允许冒险,那这时也要进行一次Full GC。

参考
Full GC

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【JVM】JVM 垃圾收集器与内存分配策略 的相关文章

随机推荐

  • python 调用C DLL库 传参(传递数字、字符串、数组(一维、二维)、结构体)

    usr bin env python conding utf 8 from itertools import filterfalse import PySimpleGUI as sg from ctypes import cdll from
  • 如何将tomcat的http访问,改为https方式访问

    在执行以下操作的时候 需要先了解一个证书生成工具 keytool 他是jdk自带的证书管理工具 在jdk bin目录下 可以用来生成自签名证书 导入导出证书 打印证书信息等 整个过程分为两部分 第一部分是证书的生成 第二部分是修改tomca
  • C语言 五子棋游戏(也可n子棋)

    目录 1 创造棋盘 2 玩家下棋 3 电脑下棋 4 判断输赢 赢的判断 也是最难的 行的判断 列的判断 主副对角线的判断 花费时间最长 平局或者继续 最后判断的全部代码 三个文件的代码 头文件的引用及函数的声明 测试部分 函数的实现 一个文
  • 系统架构师—软件架构设计(一)概述、构建、视图、基本架构风格

    1 软件架构概述 从需求分析到软件设计之间的过渡过程称为软件架构 只要软件架构设计好了 整个软件就不会出现坍塌性的错误 即不会崩溃 架构设计就是需求分配 将满足需求的职责分配到组件上 软件架构为软件系统提供了一个结构 行为和属性的高级抽象
  • 【机器学习】python使用支持向量机SVM

    机器学习 python使用支持向量机SVM 准备 数据集 导入SVM模块 步骤 1 读取数据集 2 划分训练样本与测试样本 3 训练SVM分类器 4 计算分类准确率 5 绘制图像 关于SVM的原理知识 在 机器学习 支持向量机中讲过 欲知详
  • 最近火出圈的GPT-4 技术Report出来了,快进来看看逐文对照翻译!

    近期OpenAI发布的GPT 4的效果好得让人惊艳 碾压了之前火到出圈的ChatGPT 通过同步发布的GPT 4 Technical Report一同看看到底发生了什么 No 0 摘要 We report the development o
  • 微服务网关 —— SpringCloud Gateway

    Gateway 简介 Spring Cloud Gateway 基于 Spring 5 Spring Boot 2 和 Project Reactor 等技术 是在 Spring 生态系统之上构建的 API 网关服务 Gateway 旨在提
  • 剑指offer 06 从尾到头打印链表

    题目 从尾到头打印链表 输入一个链表的头节点 从尾到头反过来返回每个节点的值 用数组返回 示例 输入 head 1 3 2 输出 2 3 1 题解一 栈 解法 遍历链表 将所有遍历到的值压入栈中 再利用栈 后进先出 的特性 从尾到头打印链表
  • STM32 进阶教程 1 - micropython 移植

    前言 Python是一种解释型 面向对象 动态数据类型的高级程序设计语言 Python 是一个高层次的结合了解释性 编译性 互动性和面向对象的脚本语言 具有如下特点 1 易于学习 Python有相对较少的关键字 结构简单 和一个明确定义的语
  • ChatGPT Prompting开发实战(五)

    一 如何编写有效的prompt 对于大语言模型来说 编写出有效的prompt能够帮助模型更好地理解用户的意图 intents 生成针对用户提问来说是有效的答案 避免用户与模型之间来来回回对话多次但是用户不能从LLM那里得到有意义的反馈 本文
  • outbound和inbound关系

    Inbound PCI域訪问存储器域 Outbound 存储器域訪问PCI域 RC訪问EP RC存储器域 gt outbound gt RC PCI域 gt EP PCI域 gt inbound gt EP存储器域 EP訪问RC EP存储器
  • python实现主成分估计

    什么是PCA 主成分分析的主要目的是希望用较少的变量去解释原来资料中的大部分变异 将我们手中许多相关性很高的变量转化成彼此相互独立或不相关的变量 通常是选出比原始变量个数少 能解释大部分资料中的变异的几个新变量 即所谓主成分 并用以解释资料
  • python编程基础知识

    python 切片 可以对list对象 如 1 2 3 4 字符串对象 1234 进行切片 使用 str l r str截取索引范围为 l r 索引值可以为负 表示从倒数方向 如 1表示倒数第一项 例 str 123456 str 0 2
  • 微信小程序开发之数据存储 参数传递 数据缓存

    微信小程序开发内测一个月 数据传递的方式很少 经常遇到页面销毁后回传参数的问题 小程序中并没有类似Android的startActivityForResult的方法 也没有类似广播这样的通讯方式 更没有类似eventbus的轮子可用 现在已
  • sql_labs18

    刚拿到题目时一点头绪没有 虽然提示是user agent注入 但没登录之前是看不到有关信息的 之后经过查看知道了两个admin就可以登录上 并且可以查看到user agent信息 判断闭合符 在User Agent字段结束添加单引号 触发报
  • esp32开发板学习

    1 esp32简介 esp32说到底就是一个小型的linux 可以执行我们的代码 尺寸只有一个苹果watch se的大小 可以集成各个物理组件 好像是通过开发板上的引脚来操作的 2 开发板部署python环境 2 1 在pdd花10块钱买了
  • 关闭文件指针不对

    浏览代码时看到下面几行代码 大家看看有啥问题 其中隐含的问题是关闭空的文件指针 所以写了一个测试代码 运行一下 挂了
  • C++模板类重载"<<"未定义错误

    在使用C 的模板类进行编程的时候 重载 lt lt 运算符时 如果定义不当 会出现未定义的情况 错误为LNK2019 这个问题的原因是由于C 的模板编译机制造成的 解决问题的方式是在类中声明 lt lt 运算符时 需要在运算符和参数之间的位
  • 【Python-利用动态二维码传输文件(五)】动态二维码文件发送端开发,使用Tkinter filedialog实现任意格式文件选中,并显示发送状态

    之前四篇文章论证了利用二维码传输文件的可行性 本章使用tkinter开发 动态二维码文件发送端 发送端具备文件选择 开始发送文件 停止发送文件以及显示发送状态的功能 程序界面下 这里下载源码运行 使用tkinter开发动态二维码文件发送端
  • 【JVM】JVM 垃圾收集器与内存分配策略

    JVM 垃圾收集器与内存分配策略 由JVM内存区域可知Java运行时内存的各个区域 其中程序计数器 虚拟机栈 本地方法栈3个区域随线程而生 随线程而灭 当方法结束或者线程结束时 内存就会跟着被回收了 而只有处于运行期间 我们才能知道程序究竟