深入解析Java垃圾回收机制

目录

一、什么是垃圾回收

二、为什么需要垃圾回收

三、Java堆内存结构与分代模型

1、新生代(Eden区+Survivor区)

2、老年代

四、常见的垃圾回收算法

[1、标记 - 清除算法](#1、标记 - 清除算法)

2、复制算法

[3、标记 - 整理算法](#3、标记 - 整理算法)

五、分代回收的核心思想与工作流程

[六、Minor GC 和 Full GC 的区别](#六、Minor GC 和 Full GC 的区别)

七、垃圾回收器的典型实现


一、什么是垃圾回收

垃圾回收****是指自动清理程序中不再使用的内存,把 " 没用的内存 " 回收再利用,避免内存泄漏、程序卡顿

  • **Java、Go、Python、C#:**自带垃圾回收,自动管理内存
  • **C/C++:**没有GC,需要程序员手动申请、手动释放内存,忘记释放就会导致内存泄漏

**缺点:**GC工作时会短暂暂停程序,对高实时场景有轻微影响


二、为什么需要垃圾回收

  1. 手动管理内存极其容易出错(人是靠不住的)
    像C++这种没有GC的语言,需要手动申请内存、手动释放内存
  2. 没有GC大程序很难写出来,GC让开发者只关注业务逻辑,不用管内存死活
    像C++这种则依靠手动管理 + 智能指针
  3. 没有GC,内存碎片会让程序卡死
    手动释放会使内存 " 碎片化 ",如果想要申请一段连续的内存,会申请不成功

垃圾回收的具体流程(回收机制):

  1. 找出谁是垃圾(不再使用的对象)
  2. 释放垃圾对象对应的内存

在Java中,判断一个对象是否是垃圾,就是找这个对象是否有引用指向,如果一个对象没有引用指向,此时就可以认为这个对象不再使用了,像下面的代码只有a,b,c都为null时Test对象才可被释放

java 复制代码
Test a = new Test();
Test b = a;
Test c = b;

第一种方案为:引用计数

引用计数不是Java使用的方案,而是Python / PHP使用的方案

对每个对象增加一个空间,这个空间存储一个整数,表示指向这个对象的引用个数,围绕对象进行引用复制时,会更新这个计数,在一定时间后如果计数是0,那么这个对象就可以释放

引用计数缺点:

  1. 可能会消耗更多的内存空间
    如果对象本身很大,那么计数器可以忽略不计,如果对象本身只有四字节,计数器占了两字节,那么就占了50%的空间
  2. 产生循环引用,会导致误判

第二种方案为:可达性分析

在Java中,一系列对象、引用存在一定的关系,类似于一个树型结构(本质是有向图遍历),从" 根节点 "出发,所有可以通过引用找到的对象 = 存活,找不到的 = 垃圾

现在以简单的代码举例:

java 复制代码
class Test{
   A a = new A();
   B b = new B();
   C c = new C();
}

Test t = new Test();

class A{
  D d = new D();
  E e = new E();
}

class B{
  F f = new F();
}

....

此代码所映射的树型结构为:

可达性分析就是从树根节点出发(实际上有多个),尝试遍历这个对象树,遍历过程中凡是经过的对象,都标记为可达,JVM知道自己一共有多少对象,除了可以到达的,剩下的就是不可达

上述 " 可达性分析 " 需要周期性进行,对象的引用是实时变化的,一轮GC需要尽可能将GC Roots遍历,尽可能标记可达

我们需要了解两个概念:

  1. GC Roots(GC根节点)
    系统认定一定不能回收的对象
  2. 引用链
    对象之间的引用关系(如上述A指向D、E)

有哪些对象可以当 GC Roots?

  • 栈上的局部变量(栈有很多,其上的栈帧也有很多,每个栈帧中局部变量也有很多)
  • 常量池引用指向的对象(Integer 值 -128~127 会提前创建对象)
  • 所有的引用类型静态成员

可达性分析优缺点:

  1. 没有额外的内存空间消耗,解决了循环引用
  2. 会消耗更多的CPU资源,是在用时间换空间

三、Java堆内存结构与分代模型


1、新生代(Eden区+Survivor区)

YGC(Minor GC)发生区,复制算法

  1. Eden 伊甸区
    新建对象优先分配在 Eden,Eden满触发 Minor GC
  2. Survivor 幸存区,S0,S1
    永远一块在用,一块空闲,每经过一次GC,年龄+1,默认年龄15升入老年代

2、老年代

Old(Major GC),标记整理/标记清除

  • 长期存活的对象:年龄达到阈值
  • Eden大对象直接进入老年代,避免复制开销
    老年代空间不足触发Full GC

四、常见的垃圾回收算法

1、标记 - 清除算法

把标记出来的垃圾,直接释放掉

现在对这些模块进行释放,这些被释放的内存在地址上是**" 离散的 "** (不连续),这就会导致内存碎片化 问题,在申请内存时都是申请连续的内存 ,当我们想要申请一个大内存时,会申请失败,但总空闲内存是足够的

在进程中,哪些对象是垃圾也是随机的,很可能在频繁的释放中产生大量的内存碎片


2、复制算法

将内存区域划分为两块,每次只对其中的一块进行操作,可以有效解决内存碎片问题

现在将1、3标记为垃圾,然后对2、3、4进行复制操作,将它们复制到右边的区域,最后将左边区域全部GC

这样做有两个缺点:

  1. 空间利用率很低,每次只使用50%的区域
  2. 如果存活的对象很多,那么复制的开销就很大

3、标记 - 整理算法

标记为垃圾的内存释放后,对现有的区域进行搬运

现在将2、5、7区域释放,对剩余区域进行搬运。

对于空间利用率得到了改善,但开销同样可能很大


五、分代回收的核心思想与工作流程

核心思想:依据对象存活生命周期长短分代,不同代使用不同的垃圾回收算法,优化回 收效率

堆划分为新生代Eden区+From Survivor+To Survivor )和老年代

工作流程:

  1. 新new的对象,放在伊甸区,在经过第一轮GC后,绝大多数对象会被淘汰掉(经验规律:大部分对象生命周期很短("朝生夕死"))
  2. 没有淘汰掉的对象通过复制算法,进入到幸存区(幸存区有两个部分,每次只使用其中的一块),下一轮GC会对幸存区对象进行扫描,还会淘汰掉一部分
  3. 没有被淘汰的对象,进入到另一个幸存区,由于每轮GC会淘汰掉大部分对象,所以进行复制算法的对象就不会很多,这就解决了复制算法开销大的问题,另一方面幸存区只占到新生代的20%,浪费的空间就比较小了
  4. 随着GC的周期性进行,对象每进行一次拷贝,其年龄就 +1
  5. 经过一定时间后,对象的年龄达到一定阈值,会拷贝进入老年代
  6. 对象进入老年代后,进行GC的频率就降低了,虽然整理一次的开销较大,但频率比较低
  7. 如果某个对象所占内存较大,就会直接进入老年代

六、Minor GC 和 Full GC 的区别

它们都是Java垃圾回收的核心机制

  • Minor GC是针对新生代GC,触发条件是Eden区满,使用复制算法,特点是频率高,速度快,时间短
  • Full GC是针对新生代+老年代+元空间,触发条件是老年代空间不足,特点是频率低,速度慢,时间长

七、垃圾回收器的典型实现

上述的分代回收是一个简化版本,也是一种思想方法

在垃圾回收时候需要考虑到效率问题,需要考虑对业务代码是否有影响

JVM垃圾回收器:

  1. CMS:并发收集,多停顿,尽可能进行多线程标记,尽可能不影响业务逻辑
    缺点:内存碎片化,占用CPU
  2. G1:分代+分区回收,能够处理内存空间特别大的情况,将整个内存区域划分出更多的空间,一次GC只回收其中的一部分
  3. ZGC:新增分代设计,追求亚毫秒级STW,支持TB级超大内存

相关推荐
浮芷.1 小时前
鸿蒙PC端 TTS 并发调用问题详解:资源竞争与队列管理
算法·华为·开源·harmonyos·鸿蒙·鸿蒙系统
YsyaaabB1 小时前
LangChain作业二---多语言翻译Prompt
开发语言·python·langchain
SunnyDays10111 小时前
如何在 Java 中实现 OFD 与 PDF 格式互转
java·开发语言
装不满的克莱因瓶1 小时前
掌握感知器的学习原理
人工智能·python·神经网络·算法·ai·卷积神经网络
Lsk_Smion1 小时前
力扣实训 _ [994].腐烂的橘子/图论
算法·leetcode·图论
轻微的风格艾丝凡1 小时前
两电平三相VSC整流模式从不控整流平滑切换至有源整流调试记录
算法·dsp·c2000
keykey6.1 小时前
用 PyTorch 训练图像分类器:完整实战
开发语言·人工智能·深度学习·机器学习
雪度娃娃1 小时前
转向现代C++——保证const成员函数的线程安全性
开发语言·c++
dongf20192 小时前
R语言KNN算法
算法·数据分析·r语言