Java 虚拟机 | 垃圾回收机制 | 七日打卡

简介: Java 虚拟机 | 垃圾回收机制 | 七日打卡

目录

image.png


前置知识


这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


1. 垃圾回收概述


垃圾回收机制(Garbage Collection,GC) 是一种自动的内存管理机制,即:当内存中的对象不再需要时,就自动释放以让出存储空间。


垃圾回收机制是 Java 虚拟机的重要特性之一,同时也是面试重要考点之一。在实践中,由于 GC 会占用程序运行资源,欲进行更有深度的内存性能优化也需要对垃圾回收机制有一定理解。


在讨论垃圾回收机制的时候,需要讨论的以下三个问题,你可以带着这三个问题阅读后面的内容,思路会更清晰。


  • 回收的对象: 哪些对象 / 区域需要回收?
  • 回收的时机: 什么时候触发 GC?
  • 回收的过程: 如何回收?


1.1 GC 相关概念


这一节,我们先罗列一些 GC 相关知识中比较重要的概念:


概念 描述
collector 表示程序中负责垃圾回收的模块
mutator 表示程序中除了 collector 以外的模块
增量式回收(Incremental Collection) 每次 GC 只针对堆的一部分,而不是整个堆,大幅减少了停顿时间
分代回收(Generational GC) 增量式回收的实现方式之一,将堆分为新生代、老生代和永生代等部分
并行回收(Parallel Collection) collector 中有多个垃圾回收线程
并发回收(Concurrent Collection) 指垃圾回收工作的某个阶段,collector 线程和 mutator 可以同时执行。
这样避免了 collector 线程工作时需要暂停 mutator 线程(stop-the-world)


1.2 垃圾回收的优缺点


  • 优点: 不再需要为每个 new 操作编写对应的 delete / free 操作,程序不容易出现内存泄漏或内存溢出问题;
  • 风险: 垃圾回收处理程序本身也占用系统资源(CPU 资源 / 内存),增大程序暂停时间。


1.3 GC 算法性能指标


在介绍垃圾回收算法之前,我们先来定义评价垃圾回收方法的性能指标:


指标 定义 描述
吞吐量(throughput) 指单位时间内的处理能力 吞吐量=运行用户代码时间垃圾回收频率∗单次垃圾回收时间吞吐量 = \frac{运行用户代码时间} {垃圾回收频率 * 单次垃圾回收时间}=
最大暂停时间(pause time) 指因执行 GC 而暂停执行程序的最长时间 /
堆利用率(space overhead) 指有效使用的堆空间占整个堆的比例 影响因素:对象头大小 + 回收算法
访问局部性 指回收方法是否倾向于访问局部内存 访问局部内存更容易命中 CPU 缓存行


提示: 若不理解 “访问局部性” 的概念,可联想快速排序和堆排序的性能对比,前者的访问局部性更优。


2. 垃圾回收管理的区域(回收的对象)


根据《Java虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下区域:


运行时数据区域 线程独占 描述
程序计数寄存器 私有 存储下一条字节码指令的内存地址
Java 虚拟机栈 私有 存储线程栈帧(Stack Frame )

栈帧包含:局部变量表、操作数栈、动态连接、返回地址等信息
本地方法栈 私有 存储本地方法栈帧
Java 堆 共享 大多数对象的存储区域
方法区 共享 存储类型信息、常量、类静态变量、即使编译器编译后的代码缓存等


并不是 Java 虚拟机管理的所有区域都需要垃圾回收,线程独占的区域会随着线程结束而销毁,不需要垃圾回收。因此垃圾回收机制需要管理的区域是:


  • 堆: 垃圾对象;
  • 方法区: 废弃的常量和不再使用的类型。


3. 如何判定垃圾对象?(回收的时机)


判断对象是否为垃圾对象的方法可以分为两种:引用计数 & 可达性分析。以判断方法为划分,后文所讲的垃圾回收算法也可以划分为 引用计数式 & 追踪式 两大类。


3.1 引用计数算法(Reference Counting)


3.1.1 判定方法

在分配对象时,会额外为对象分配一段空间,用于记录指向该对象的引用个数。如果有一个新的引用指向该对象,则计数器加 1;当一个引用不再指向该对象,则计数器减 1 。当计数器的值为 0 时,则该对象为垃圾对象。


3.1.2 优点

  • 1、及时性:当对象变成垃圾后,程序可以立刻感知,马上回收;而在可达性分析算法中,直到执行 GC 才能感知;
  • 2、最大暂停时间短:GC 可与应用交替运行。


3.1.3 缺点

  • 1、计数器值更新频繁:大多数情况下,对象的引用状态会频繁更新,更新计数器值的任务会变得繁重;
  • 2、堆利用率降低:计数器至少占用 32 位空间(取决于机器位数),导致堆的利用率降低;
  • 3、实现复杂;
  • 4、(致命缺陷)无法回收循环引用对象。


易错: 引用计数法是算法简单,实现较难。


3.2 可达性分析算法(Reachability Analysis)


3.2.1 判定方法


从 GC 根节点(GC Root)为起点,根据引用关系形成引用链。当一个对象存在到 GC Root 的引用链,则为存活对象,否则为垃圾对象。在 Java 中,GC Root 主要包括:


  • 1、Java 虚拟机栈中引用的对象(即栈帧中的本地变量表);
  • 2、本地方法栈中引用的对象;
  • 3、方法区中类静态变量引用的对象;
  • 4、方法区常量池中引用的对象;
  • 5、同步锁(synchronized 关键字)持有的对象;


3.2.2 优点


  • 1、可回收循环引用对象;
  • 2、实现简单。


3.2.3 缺点


  • 1、最大停顿时间长:在 GC 期间,整个应用停顿(stop-the-world,STW);
  • 2、回收不及时:直到执行 GC 才能感知垃圾对象;


3.3 小结


判定方法 优点 缺点
引用计数 1、及时性
2、最大暂停时间短
1、计数器值更新频繁
2、堆利用率降低
3、实现复杂
4、无法回收循环引用对象
可达性分析 1、可回收循环引用对象
2、实现简单
1、最大停顿时间长
2、回收不及时


由于引用计数式 GC 存在 「无法回收循环引用对象」 的致命缺陷,工业实现上还是追踪式 GC 占据了主流,后面我主要介绍的也是追踪式 GC。


4. 垃圾回收算法(回收的过程)


从原理上,垃圾回收算法可以分为以下四类基础算法,其它的垃圾回收算法其实是对基础算法的改进或组合。


时间 早期提出者 算法 类别
1960年 Lisp 之父 John McCarthy 标记 - 清理算法 追踪式
1960年 George E. Collins 引用计数算法 引用计数式
1969年 Fenichel 复制算法 追踪式
1974年 Edward Lueders 标记 - 整理算法 追踪式


在实践中,当代绝大多数垃圾收集器都采用了 “分代收集模型” ,该模型的经验前提是:


  • 1、绝大多数对象都是朝生夕死,无法熬过第一次垃圾回收;
  • 2、熬过了多次垃圾回收的对象,往往越难被回收。


在上述事实经验的基础上,虚拟机往往使用了 动静分离 的设计思想:将新对象和难以回收的老对象存储在不同的区域,新对象存放在新生代,难回收的对象存在老年代。并且针对不同区域的特性采用不同的垃圾回收算法。

image.png


—— 图片引用自网络


  • 1、新生代: 新生代中的对象存活率低,只要付出少量的复制成本就能完成回收过程,因此选用复制算法;
  • 2、老生代: 老生代中的对象存活率高,并且没有额外空间进行分配担保,因此选用 “标记 - 清理” 或 “标记 - 整理” 算法。


4.1 标记 - 清理算法(Mark-Sweep)


4.1.1 算法回收过程


标记 - 清理算法的回收过程主要分为两个阶段:

  • 标记(Mark)阶段: 遍历整个堆,标记出垃圾对象(也可以标记存活对象);
  • 清理(Sweep)阶段: 遍历整个堆,将垃圾对象分块链接空闲列表。


image.png

4.1.2 优点


实现简单;

4.1.3 缺点


  • 1、执行效率不稳定:Java 堆中对象越多,标记和清理的过程可能会越耗时;
  • 2、内存碎片化(fragmentation):回收过程会逐渐产生很多不连续的小内存,当小内存不足以分配对象内存时,又会触发一次垃圾回收动作(GC for Alloc)。


4.2 复制算法(Copying)


4.2.1 算法回收过程


复制算法的回收过程要点如下:


  • 1、将堆分为大小相同的两个空间:from 区和 to 区;
  • 2、对象的内存分配只使用 from 区,当 from 区占满时,将存活对象全部复制到 to 区;
  • 3、复制完成后互换 from 区和 to 区的指针。


image.png

—— 图片引用自 weread.qq.com/web/reader/… 邓凡平 著


4.2.2 优点

  • 1、快速分配对象:空闲分块是一个连续内存空间,不需要向标记-清理算法那样遍历空闲列表;
  • 2、避免内存碎片化:存活对象和新分配对象都被压缩到 tospace 的一端,避免出现很多不连续的小内存。


4.2.3 缺点

  • 1、堆利用率低:把堆做二等分只能利用其中的一半,堆利用率最高仅为 50 %。


4.2.4 改进

  • 1、将新生代分为:一块 Eden 区和两块 Survivor 区,对应的比例为 8:1:1;
  • 2、对象只在 Eden 区分配,当 Eden 区占满后,将 Eden 区和 from Survivor 区的存活对象全部赋值到 to Survivor 区;
  • 3、复制完成后互换 from Survivor 区和 to Survivor 区的指针。

改进后堆利用率提升到最高 90%。


4.3 标记 - 整理算法(Mark-Compact)


4.3.1 算法回收过程


标记 - 清除算法与标记 - 整理算法的本质差异在于是否移动对象。标记 - 整理算法的回收过程主要分为两个阶段:


  • 标记(Mark)阶段: 遍历整个堆,标记出垃圾对象(这个步骤与标记 - 清理算法相同);
  • 整理(Compact)阶段: 将所有存活对象移动(压缩)到堆的一端,然后直接清理掉边界以外的内存。


image.png

—— 图片引用自 weread.qq.com/web/reader/… 邓凡平 著

4.3.2 优点

  • 1、避免内存碎片化,堆利用率高,吞吐量更高;
  • 2、快速分配对象:空闲分块是一个连续内存空间,不需要向标记-清理算法那样遍历空闲列表;


4.3.3 缺点

  • 1、移动对象比清理对象更耗时,导致 GC 停顿时间(Stop-the-world)时间更长。


5. 并发回收


5.1 stop-the-world 现象


在标准的垃圾回收算法中,在垃圾回收线程(collector)进行标记 - 清理 / 整理 / 复制的过程中需要暂停所有的用户线程(mutator),这是为了保证能够彻底清理所有垃圾对象。


但是这种做法却会导致虚拟机的吞吐量降低(吞吐量=运行用户代码时间垃圾回收频率∗单次垃圾回收时间吞吐量 = \frac{运行用户代码时间} {垃圾回收频率 * 单次垃圾回收时间}=)。


5.2 CMS 垃圾收集器


在追求响应速度的系统上,希望垃圾收集器暂停时间尽可能小,为此发展出了允许回收线程与用户线程并发运行的垃圾收集器 —— CMS(Concurrent Mark Sweep,并发标记清除)。


CMS 垃圾收集器的主要工作过程分为 4 个步骤:


  • 1、初始标记(短暂 stop-the-world): 仅仅标记被 GC Root 直接引用的对象,由于 GC Root 相对较少,这个过程速度很块;
  • 2、并发标记(耗时): 继续遍历 GC Root 引用链上的对象,这个过程比较耗时,所以采用并发处理;
  • 3、重新标记(短暂 stop-the-world): 为了修正并发标记期间用户线程导致的引用关系变化,需要暂停用户线程重新标记;
  • 4、并发清除(耗时) 由于清除对象的过程比较耗时,所以采用并发处理。


image.png


—— 图片引用自网络

5.3 CMS 的优点


  • 1、缩短了系统 stop-the-world 时间,提高了吞吐量;


5.4 CMS 的缺点


  • 1、CPU 敏感: 采用了并发策略,系统整体上会占用更多 CPU 资源;
  • 2、浮动垃圾: 由于并发清理的过程中用户线程还在运行,CMS 无法回收这个阶段中用户线程产生的垃圾,这一部分垃圾称为 “浮动垃圾”。由于浮动垃圾的存在,垃圾收集器需要预留出一部分空间来允许浮动垃圾的产生,如果预留的空间还不足以存放浮动垃圾,就会出现 Concurrent Mode Failure,此时需要临时启动非并发清理方案来代替 CMS;
  • 3、内存碎片: 采用标记 - 清理算法,会产生内存碎片。


6. 总结


  • 1、垃圾回收算法的性能指标主要有:吞吐量、最大暂停时间、堆利用率、访问局部性。在理解垃圾回收机制的过程中,可以带着 “回收的对象” & “回收的时机” & “回收的过程” 三个问题来理解;
  • 2、垃圾回收机制管理的区域有堆和方法区;
  • 3、判断垃圾对象的算法分为引用计数算法和可达性分析算法,两者各有优缺点;
  • 4、垃圾回收算法可以分为四类基本算法:引用计数算法、标记-清理算法、标记-整理算法和复制算法。其它的垃圾回收算法都是对基础算法的改进或组合。比如主流的虚拟机垃圾回收算法采用分代回收模型:即在新生代选用复制算法(对象存活率低),而老生代选用 “标记 - 清理” 或 “标记 - 整理” 算法(对象存活率高,并且没有额外空间进行分配担保);
  • 5、在标准的垃圾回收算法中,垃圾回收过程会 stop-the-world。使用并发收集可以降低系统的暂停时间,提供吞吐量。
目录
相关文章
|
6天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
21 6
|
28天前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
18天前
|
监控 算法 Java
深入理解Java的垃圾回收机制
【10月更文挑战第22天】在Java的世界里,有一个默默无闻却至关重要的角色——垃圾回收(Garbage Collection, GC)。就像城市的清洁工一样,它默默地清理着不再使用的内存空间,确保我们的程序运行得既高效又稳定。但你真的了解垃圾回收是如何工作的吗?让我们一起探索这个看似简单却充满奥秘的过程,看看它是如何影响你的Java应用性能的。
|
20天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
27天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
30天前
|
存储 监控 算法
深入理解Java内存模型与垃圾回收机制
【10月更文挑战第10天】深入理解Java内存模型与垃圾回收机制
19 0
|
30天前
|
监控 算法 Java
Java中的垃圾回收机制深度解析
【10月更文挑战第10天】 本文深入探讨了Java语言核心特性之一的垃圾回收机制(Garbage Collection, GC),揭示了其在内存管理中的关键角色。通过对GC的工作原理、分类、算法以及调优策略的细致分析,旨在帮助开发者更好地理解并有效利用这一机制,提升Java应用的性能与可靠性。不同于常规摘要,本文聚焦于为读者提供一份关于Java GC全面而深入的解读,助力把握Java内存管理的精髓。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
4天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
6 1