JVM专题——垃圾回收

本文部分内容节选自Java Guide和《深入理解Java虚拟机》, Java Guide地址: https://javaguide.cn/java/jvm/jvm-garbage-collection.html

🚀 基础(上) → 🚀 基础(中) → 🚀基础(下) → 🤩集合(上) → 🤩集合(下) → 🤗JVM专题1 → 🤗JVM专题2 → 🤗JVM专题3 → 🤗JVM专题4

堆内存基本结构

回顾堆内存的结构. Java 自动内存管理的主要区域是 Java 堆, 因此 Java 堆也被称为 GC 堆

在 JDK1.7及以前的版本, 堆内存分为以下三部分:

  1. 新生代
  2. 老年代
  3. 永久代

在这里插入图片描述

JDK1.8 之后, 永久代被元空间取代

具体的关于堆内存基本结构相关信息, 请看JVM专题3

内存分配与回收原则

对象优先在Eden区分配

大多数情况下, 对象在新生代中Eden区分配, 当Eden区没有足够空间进行分配时, 虚拟机会发起一次 Minor GC. 如果执行 Minor GC 之后, Eden区足够存储对象, 那么就会在Eden区分配对象内存; 否则会通过 分配担保机制 将新生代对象暂时存储到老年代

大对象直接进入老年代

大对象就是指需要大量连续内存空间的对象(例如字符串, 数组)

大对象直接进入老年代的行为是由虚拟机动态决定的, 它与具体使用的垃圾回收器和相关参数有关. 大对象进入老年代是一种优化策略, 旨在避免将大对象放入新生代, 从而减少新生代的垃圾回收频率和成本

  • G1垃圾回收器会根据 -XX:G1HeapRegionSize 参数设置堆区域的大小和 -XX:G1MixedGCLiveThresholdPercent 参数设置的阈值, 来决定哪些对象会直接进入老年代
  • Parallel Scavenge垃圾回收器中, 默认情况下, 是没有一个固定的阈值来决定何时直接在老年代分配大对象, 而是由虚拟机根据当前的堆内存情况和历史数据动态决定

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

虚拟机给每个对象都分配了一个对象年龄计数器. 大部分情况下, 对象首先在 Eden 区分配, 如果对象在 Eden区出生且经过第一次 Minor GC后仍能存活, 且能被Survivor收纳的话, 将被移动到 Survivor空间, 并将对象年龄设置为1

对象每在Survivor熬过一次Minor GC, 年龄就会增长1岁. 当它的年龄增加到一定程度(默认为15), 就会被晋升到老年代.

主要进行GC的区域

部分收集 (Partial GC):

  • 新生代收集(Minor GC/Young GC): 只对新生代进行垃圾收集
  • 老年代收集(Major GC/Old GC): 只对老年代进行垃圾收集, 注意Major GC在有些语义下也指整堆收集
  • 混合收集(Mixed GC): 对整个新生代和部分老年代进行垃圾收集

整堆收集(Full GC): 收集整个Java堆和方法区

空间分配担保

空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

JDK6 Update24 之前, 在发生Minor GC之前, 虚拟机必须检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立, 那么这一次Minor GC可以保证是安全的, 如果不成立, 则虚拟机会先查看 -XX:HandlePromotionFailure 参数的设置值是否允许担保失败, 如果允许, 则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于, 则尝试进行一次Minor GC; 如果小于, 或者 -XX:HandlePromotionFailure 被设置为是不允许的, 那么这一次就要进行Full GC

JDK6 Update24之后, 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小, 就会进行 Minor GC, 否则进行 Full GC

对象死亡判断

引用计数算法

在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值减一; 任何时刻计数器为零的对象就是不可能再被使用的, 这一点有点类似于C++智能指针中的shared pointer共享指针

它的原理很简单, 且效率很高, 但实际上主流的Java 虚拟机都没有使用这个算法来管理内存, 因为这个算法无法解决对象之间循环引用的问题. 如果对象a和对象b互相引用, 除此之外没有其他对象再引用这两个对象, 但是由于引用计数器不为零, 所以使用引用计数算法无法让垃圾收集器回收它们

可达性分析

目前主流的支持垃圾回收的商用程序语言都是通过 可达性分析 来判断对象是否存活

这个算法的基本思路就是通过一系列称为 "GC Roots"的根对象作为起始节点集, 从这些结点开始, 根据引用关系向下搜索, 搜索过程中所走过的路径称为 “引用链” . 如果从GC Roots到某个对象没有一条通路, 那么说明这个对象已经死亡了

在这里插入图片描述

例如, 上图的 Object6~Object10 之间虽然互有引用关系, 但是由于它们到GC Roots没有一条通路, 所以它们都是属于要被回收的对象

可以作为GC Roots的对象:

  • 在虚拟机栈中引用的对象, 例如各个线程中被调用的方法堆栈中使用的参数, 局部变量, 临时变量等
  • 在方法区中类静态属性引用的对象, 例如Java类的引用类型静态变量
  • 在方法区中常量引用的对象, 例如字符串常量池中的引用
  • 在本地方法栈中JNI引用的对象
  • Java 虚拟机内部的引用, 如基本数据类型对应的Class 对象, 一些常驻的异常对象, 系统类加载器
  • 所有被同步锁持有的对象
  • 反应Java虚拟机内部情况的JMXBean, JVMTI中注册的回调,本地代码缓存等

对象可以被回收, 就代表一定会被回收吗?

真正宣告一个对象"死亡", 需要经过两个阶段, 可达性分析法不可达的对象第一次标记并进行一次筛选, 筛选的条件是这个对象是否有必要执行 finalize 方法, 当对象没有覆盖 finalize 方法, 或 finalize 方法已经被虚拟机调用过时, 虚拟机将这两种情况视为没有必要执行

被判定为需要执行的对象会被放在一个队列里进行第二次标记, 除非这个对象与引用链上的任何一个对象建立关联, 否则就会被真的回收

引用

JDK1.2之后, Java的引用分为 强引用 , 弱引用 , 软引用 , 虚引用 四类

  • 强引用: 强引用是最传统的引用关系, 无论任何情况下, 只要强引用关系存在, 垃圾收集器就不会回收掉被引用的对象. 当内存空间不足, 虚拟机宁愿抛出OutOfMemoryError错误, 也不会随意回收具有强引用的对象

  • 软引用: 软引用用来描述一些还有用, 但非必须的对象. 只被软引用关联着的对象, 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围之中进行第二次回收, 如果这一次回收还没有足够的内存, 才会抛出内存溢出异常.

  • 弱引用: 弱引用也用来描述一些非必须对象, 但是它的强度比软引用更弱, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止

  • 虚引用: 虚引用是最弱的引用关系. 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来获得一个对象实例. 为一个对象设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

    虚引用和软引用, 弱引用的差别在于: 虚引用必须与引用队列联合使用, 当垃圾回收器准备回收一个对象时, 如果发现它还有虚引用, 就会在回收对象的内存之前, 把这个虚引用加入到与之关联的引用队列之中. 程序可以通过判断引用队列中是否已经加入了虚引用, 来了解这个对象是否将要被垃圾回收

实际上使用软引用的情况比使用弱引用, 虚引用的情况要多得多. 这是因为软引用可以加速JVM对垃圾内存的回收速度, 避免出现内存溢出等问题

如何判断一个类是无用的类?

  • 该类所有的实例都已经被回收, 也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法

垃圾回收算法

标记-清除算法

标记-清除算法分为 “标记” 和 “清除” 阶段: 首先标记出所有不需要回收的对象, 在标记完成之后统一回收掉所有没有被标记的对象

它会带来两个明显的问题

  1. 效率问题 : 标记和清除两个过程效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片

在这里插入图片描述

标记-复制算法

为了解决标记-清除算法面对大量可复制对象时执行效率低的问题, 标记-复制算法诞生了. 它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块. 当这一块的内存用完了就将还活着的对象复制到另一块内存上, 然后再把已经使用过的内存空间清理掉.

在这里插入图片描述

但是这个算法仍然存在一些问题

  1. 可用内存变小 : 可用内存缩小为原来的一半
  2. 不适合老年代 : 如果存活对象数量比较大, 复制性能会变得很差

标记-整理算法

标记-整理算法是根据老年代的特点提出的一种标记算法, 标记过程仍然和标记-清除算法一样, 但后续步骤不是直接对可回收对象回收, 而是让所有存活的对象向一端移动, 然后直接清理掉端边界以外的内存

在这里插入图片描述

因为多了整理这一步, 所以效率也不高, 适用于老年代这种垃圾回收频率不高的场景

分代收集算法

一般将Java堆分为新生代和老年代, 这样我们就可以根据各个年代的特点选择合适的垃圾收集算法

垃圾收集器

JDK默认垃圾收集器

  • JDK8: Parallel Scavenge(新生代) + Parallel Old(老年代)
  • JDK9~JDK20: G1

Serial 收集器

Serial 收集器是一个单线程收集器, 它在进行垃圾收集工作时候必须暂停其他所有的工作线程, 直到它收集结束

新生代采用标记-复制算法, 老年代采用标记-整理算法

Serial 收集器简单且高效, 且因为没有线程交互的开销, 自然可以获得很高的单线程收集效率. Serial收集器对于运行在客户端模式的虚拟机来说是一个不错的选择
在这里插入图片描述

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本, 除了使用多线程之外, 其余行为和 Serial 收集器完全一样

它是许多运行在服务端模式下虚拟机的首要选择, 除了 Serial 收集器外, 只有它能和CMS收集器配合工作

在这里插入图片描述

Parallel Scavenge 收集器

Parallel Scavenge 收集器 也是使用标记-复制算法的多线程收集器

Parallel Scaveng 收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量. 吞吐量就是CPU中用于运行用户代码的时间和CPU总消耗时间的比值. Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间和最大吞吐量, 如果对于收集器工作不了解, 手工优化存在困难时, 可以使用 Parallel Scavenge 收集器配合自适应调节策略

新生代使用标记-复制算法, 老年代使用标记-整理算法

在这里插入图片描述

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本, 它也是单线程收集器, 使用标记-整理算法. 这个收集器的主要意义是供客户端模式下的HotSpot虚拟机使用. 在服务端模式下, 它有两种用途: 一种是在 JDK5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用, 另外一种就是作为CMS收集器发生失败时的后备预案

在这里插入图片描述

Parallel Old 收集器

Parallel Old 是 Parallel 收集器的老年代版本, 支持多线程并发收集, 使用标记-整理算法

在这里插入图片描述

CMS 收集器

CMS 收集器是一种以获取最短回收停顿时间为目标的收集器. 非常适合在注重用户体验的应用上使用

CMS 收集器也是 HotSpot虚拟机第一款真正意义上的并发收集器, 实现了垃圾收集线程和用户现场同时工作

CMS收集器是采用标记-清除算法实现的, 收集过程分为4个步骤

  1. 初始标记 : 暂停所有线程, 并记录下直接与root相连的对象, 速度很快
  2. 并发标记 : 同时开启 GC 和用户线程, 用一个闭包结构去记录可达对象. 但在这个阶段结束, 这个闭包结构不能保证包含所有的可达对象. 因为用户线程可能会不断的更新引用域, 所以 GC 线程无法保证可达性分析的实时性. 所以这个算法里会跟踪记录这些发生引用更新的地方
  3. 重新标记 : 重新标记阶段就是为了修正并发标记阶段因为用户程序继续运行而导致标记变动的那一部分对象的标记记录, 这一阶段的停顿时间比初始标记阶段停顿时间长, 远比并发标记阶段时间短
  4. 并发清除 : 开启用户线程, 同时 GC 线程对未标记的区域做清扫

CMS的优点: 并发收集, 地停顿

CMS的缺点:

  1. 对CPU资源非常敏感
  2. 由于CMS收集器无法处理浮动垃圾, 所以可能导致 “Con-current Mode Failure” 失败进而导致另一次完全 “Stop the world” 的 Full GC 的产生
  3. 标记-清除算法会导致收集结束后产生大量的空间碎片

从 JDK9 开始, CMS收集器已被弃用

G1 收集器

G1 收集器是一款面向服务器的垃圾收集器, 主要针对具备多核处理器和大容量内存的机器, 以极高概率满足GC停顿时间要求的同时, 还具备高吞吐性能特征

G1 收集器在后台维护了一个优先列表, 每次根据允许的收集时间, 优先选择回收价值最大的 Region. 这种使用 Region 划分内存空间以及有优先级的区域回收方式, 保证了 G1 收集器在有限时间内可以尽可能高的收集效率

G1收集器的运作大致分为以下步骤

  1. 初始标记 : 仅仅标记 GC Roots 能直接关联到的对象, 并且修改 TAMS 指针的值, 让下一阶段用户线程并发运行时, 能正确地在可用的 Region 中分配新对象. 这个阶段需要停顿线程, 但耗时很短, 并且是借用进行 Minor GC 时同步进行的

  2. 并发标记 : 从 GC Roots 开始对堆中对象进行可达性分析, 递归扫描整个堆中的对象图, 找出要回收的对象, 这阶段耗时较长, 但可以与用户线程并发进行. 当对象图扫描完成之后, 还要重新处理STAB记录下的在并发时有引用变动的对象

  3. 最终标记 : 对用户线程做短暂的暂停, 用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录

  4. 筛选回收 : 负责更新 Region 的统计数据, 对各个 Region 的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个 Region 构成回收集, 然后把决定回收的那一部分 Region 的存活对象复制到空 Region 中, 再清理掉整个旧 Region 的全部空间. 这个操作涉及到存活对象的移动, 是必须暂停用户线程, 由多条收集器线程并行完成的

从 JDK9 开始, G1收集器为默认垃圾收集器

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024-04-05 问AI: 介绍一下深度学习中的Leaky ReLU函数

文心一言 Leaky ReLU(Leaky Rectified Linear Unit)函数是深度学习领域中的一种激活函数,主要用于解决ReLU(Rectified Linear Unit)函数在负值区域为零的问题。ReLU函数在输入为正时保持原样,而在输入为负…

(学习日记)2024.04.07:UCOSIII第三十五节:互斥量实验

写在前面: 由于时间的不足与学习的碎片化,写博客变得有些奢侈。 但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。 既然如此 不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录&a…

通过 Cookie、Redis共享Session 和 Spring 拦截器技术,实现对用户登录状态的持有和清理(三)

本篇内容对应 “2.4 生成验证码” 小节 和 “4.7 优化登陆模块”小节 视频链接 1 Kaptcha介绍 Kaotcga是一个生成验证码的工具。 你的网站验证码是什么? 在我们这个牛客论坛项目,验证码分为两部分 给用户看的是图片,用户根据图片上显示的…

跨境电商独立站是什么?为什么要做独立站?

跨境电商独立站就是跨境电商自行搭建的销售网站,服务器、域名都是自主购买的,并由跨境电商独立运营与营销推广。 近些年来,各类第三方电商平台虽然流量大,但是随着进驻电商数量的增加,流量竞争也愈发激烈,…

基于顺序表实现通讯管理系统!(有完整源码!)

​​​​​​​ 个人主页:秋风起,再归来~ 文章专栏:C语言实战项目 个人格言:悟已往之不谏,知来者犹可追 克心守己,律己则安!​​​​​​​ 目录 1、实现思路 ​…

C语言中strlen函数的实现

C语言中strlen函数的实现 为了便于和strlen函数区别,以下命令为_strlen。 描述:实现strlen,获取字符串的长度,函数原型如下: size_t strlen(const char *str);_strlen实现: size_t _strlen(const char*…

彩虹聚合DNS管理系统,附带系统搭建教程

聚合DNS管理系统,可以实现在一个网站内管理多个平台的域名解析,目前已支持的域名平台有:阿里云、腾讯云、华为云、西部数码、CloudFlare。 本系统支持多用户,每个用户可分配不同的域名解析权限;支持API接口&#xff0…

武汉星起航:跨境电商领域的领航者,助力全球贸易新篇章

自2017年以来,武汉星起航一直专注于亚马逊自营店铺,积累了宝贵的经验。2020年正式成立后,公司以跨境电商为核心,致力于为合作伙伴提供深入的合作模式。武汉星起航凭借其卓越的服务和实战经验,已成功助力众多创业者实现…

基于SpringBoot的“智慧外贸平台”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“智慧外贸平台”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体结构图 平台首页界面图 商品信息界面图 …

Java8 进阶

Java8 进阶 文章目录 Java8 进阶什么是函数式接口&#xff1f;public interface Supplierpublic interface Consumerpublic interface Predicatepublic interface FunctionJava8 特性总结&#xff1a;一、Function<T, R>二、Consumer<T>三、Supplier<T>四、P…

位运算-191. 位1的个数- 136. 只出现一次的数字

位1的个数 已解答 简单 相关标签 相关企业 编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中 设置位 的个数&#xff08;也被称为汉明重量&#xff09;。 示例 1&#xff1a; 输入&#xff1a;n 11 输…

Linux第4课 Linux的基本操作

文章目录 Linux第4课 Linux的基本操作一、图形界面介绍二、终端界面介绍 Linux第4课 Linux的基本操作 一、图形界面介绍 本节以Ubuntu系统的GUI为例进行说明&#xff0c;Linux其他版本可自行网搜。 图形系统进入后&#xff0c;左侧黄框内为菜单栏&#xff0c;右侧为桌面&…

c# 指数搜索(Exponential Search)

该搜索算法的名称可能会产生误导&#xff0c;因为它的工作时间为 O(Log n)。该名称来自于它搜索元素的方式。 给定一个已排序的数组和要 搜索的元素 x&#xff0c;找到 x 在数组中的位置。 输入&#xff1a;arr[] {10, 20, 40, 45, 55} x 45 输出&#xff1a;在索…

检验平台最基本的技术要求有哪几条

检验平台最基本的技术要求通常有以下几条&#xff1a; 系统稳定性&#xff1a;检验平台应具备良好的稳定性&#xff0c;能够长时间运行而不出现系统崩溃或异常情况。 数据安全性&#xff1a;检验平台应具备对数据进行安全存储和传输的能力&#xff0c;确保数据不被非法获取、篡…

吴恩达机器学习笔记:第 6 周-11机器学习系统的设计(Machine Learning System Design)11.1-11.5

目录 第 6 周 11、 机器学习系统的设计(Machine Learning System Design)11.1 首先要做什么11.2 误差分析11.3 类偏斜的误差度量11.4 查准率和查全率之间的权衡11.5 机器学习的数据 第 6 周 11、 机器学习系统的设计(Machine Learning System Design) 11.1 首先要做什么 在接…

基于Python的豆瓣电影评分可视化,豆瓣电影评分预测系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

2024年阿里云4月服务器有哪些优惠活动?

2024年阿里云服务器4月优惠活动有哪些&#xff1f;4月份最新优惠活动有99计划云服务器99元一年、学生服务器、游戏服务器优惠、云服务器精选特惠、高校计划优惠券300元、阿里云服务器免费试用等活动。4月云服务器最新优惠价格2核2G3M带宽99元一年、2核4G5M带宽199元一年&#x…

AcWing 312. 乌龟棋(每日一题)

原题链接&#xff1a;312. 乌龟棋 - AcWing题库 小明过生日的时候&#xff0c;爸爸送给他一副乌龟棋当作礼物。 乌龟棋的棋盘只有一行&#xff0c;该行有 N 个格子&#xff0c;每个格子上一个分数&#xff08;非负整数&#xff09;。 棋盘第 1 格是唯一的起点&#xff0c;第…

vue + koa + Sequelize + 阿里云部署 + 宝塔:宝塔数据库连接

之前文章已经介绍了宝塔上传前后端代码并部署&#xff0c;不清楚的请看这篇文章&#xff1a; vue koa 阿里云部署 宝塔&#xff1a;宝塔前后端部署 下面是宝塔创建数据库&#xff1a; 我用的 koa Sequelize 连接的数据库&#xff0c;Sequelize 非常适合前端使用&#xf…

Map源码解析

基本介绍 其实HashMap底层是个什么东西我们之前也讲过, 就是一个哈希桶(差不多可以看成一个数组), 然后每一个节点又连接着链表/红黑树之类的, 下面让我们看一看具体在源码上是怎样实现的: 常量及其它 -> static final int DEFAULT_INITIAL_CAPACITY 1 << 4; //这个…