> 文章列表 > 【GC垃圾回收算法】让内存垃圾无处藏身

【GC垃圾回收算法】让内存垃圾无处藏身

【GC垃圾回收算法】让内存垃圾无处藏身

文章目录

  • 概述
  • 垃圾对象的判定
    • 引用计数
    • 可达性分析
  • 回收垃圾
    • 标记清除
    • 复制算法
    • 标记整理
    • 分代回收

概述

垃圾回收就是帮我们把不用的内存垃圾自动释放掉

什么是垃圾呢?就是指不再使用的垃圾

如果不进行垃圾回收就会导致一个严重的问题,内存泄漏

内存泄漏: GC无法及时识别可以回收的内存进行释放,也就是内存占着不用也释放不了,导致内存的浪费,以至于后续的内存申请都会操作失败

如果内存泄漏的越多就会导致内存溢出

内存溢出: 程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中

内存泄漏是一个很严重的问题,所以大佬们就想了一些办法来解决这个问题,而GC就是其中最主流的一种方式

  • GC的好处:让程序猿写代码简单点,不容易出错
  • GC的坏处:需要消耗额外的系统资源,也有额外的性能开销

GC还有一个比较关键的问题STW(Stop The World)问题

有时候垃圾很多,触发一次GC,开销会非常大,把系统资源吃掉很多,另一方面,GC回收垃圾的时候会涉及一些锁的操作,导致业务代码无法正常执行

JVM中存在许多内存区域,像堆,栈,元数据区等,GC主要针对堆进行释放的,GC是以“对象”为基本单位进行回收的,回收的是整个对象都不再使用的这种情况,而不会回收半个对象(一部分使用,一部分不再使用)

GC的工作过程分为两个步骤:

  1. 找/判定垃圾:找到内存中不再使用的对象,判定其为垃圾
  2. 释放垃圾:找到不再使用的对象后,释放对象

垃圾对象的判定

判定对象是垃圾的关键点在于是否有引用指向它

引用计数

注意: 引用计数不是Java中采用的方法,而是Python,PHP采用的方法

引用计数就是给每个对象分配了一个计数器,如果每次创建一个引用指向该对象,则计数器+1,如果每次销毁一个引用,该计数器-1,如果计数器为0,则判定为垃圾

缺点:

  1. 内存空间利用率低,因为每个对象都要分配空间给计数器使用
  2. 存在循环引用问题
public class Test  {Test t = null;public static void main(String[] args) {Test t1 = new Test(); //对象1引用计数为1Test t2 = new Test(); //对象2引用计数为1t1.t = t2; //对戏2引用计数为2t2.t = t1; //对象1引用计数为2}
}

说明: 如果将t1和t2销毁,那么对象1和对象2的引用计数都为1,但是此时引用计数不为0,不能判定为垃圾,但实际上这两个对象已经不能被访问到了

所以Java没有采用引用计数这种方法,Python,PHP使用引用计数需搭配其它的机制来避免这种循环引用的问题

可达性分析

Java中的对象都是通过引用来指向并访问的,并且一个引用指向的对象,该对象的成员通常又指向别的对象

可达性分析就是把所有这些对象被组织的结构视为是树,从树的根节点(GCRoots)出发遍历树,所有能被访问到的对象就标记为“可达”,不能访问到的对象就是不可达
JVM有一个所有对象的名单,通过上述遍历,把能访问到的标记为可达,剩下未标记的就是不可达的就会判定为垃圾

遍历的起点称为GCRoots,通常可作为GCroots有以下几类:

  • 栈上的局部变量
  • 常量池中的对象
  • 静态成员变量

回收垃圾

标记清除

该种方法简单粗暴,直接清除垃圾
【GC垃圾回收算法】让内存垃圾无处藏身

缺点: 存在内存碎片问题,因为释放的空闲空间不是连续的,但是申请内存时要求是连续空间,需要申请大一点的内存时,总的内存是够的,但都是零散的,零散的内存不够,此时就会申请失败

复制算法

  1. 该方法是将内存分成两半,每次只用一半
    【GC垃圾回收算法】让内存垃圾无处藏身

  2. 把不是垃圾的对象复制到另一半,然后把整个空间删掉
    【GC垃圾回收算法】让内存垃圾无处藏身

  3. 每次触发复制算法都是把内存中的数据往另外一侧复制

此方法解决了内存碎片问题
缺点: 空间利用率低,如果垃圾少,有效的对象多,复制成本就很大

标记整理

此方法类似顺序表删除中间元素,会有元素搬运的操作
【GC垃圾回收算法】让内存垃圾无处藏身

让存活的对象往一端移动,直接清除存活对象边界之外的内存

保证了空间利用率,也解决了内存碎片问题
缺点: 效率低,元素搬运的空间比较大那开销也大

标记整理只解决了复制算法中空间利用率低的问题,没有解决复制成本大的问题

分代回收

上述的回收策略都不完美,所以基于上述策略搞了一个复合策略分代回收

那分代是咋分的呢?

  • 基于经验规律,如果一个对象存活时间长,那么还会长时间的存活下去
  • Java对象的生命周期要么特别短,要么特别长
  • 根据生命周期的长短,分别使用不同的算法

给对象引入一个年龄,单位为经历GC的次数,如果经历GC的次数越长,说明该对象存活的时间越长

JVM将堆划分为一系列区域
【GC垃圾回收算法】让内存垃圾无处藏身

  • 刚new出来的对象放在伊甸区(伊甸区大,幸存区小)
  • 熬过一轮GC,对象就要被放到幸存区(基于复制算法)了(Java中的大部分对象生命周期都很多,熬不过第一轮GC)
  • 在幸存区的对象,如果不是垃圾就会被拷贝到另一个幸存区(幸存区只会使用一个),然后直接清除现在的幸存区空间,后序的几轮GC都在这两个幸存区之间来回拷贝(复制算法)
  • 如果对象在幸存区来回拷贝许多次了,那么该对象就会进入老年代
  • 针对老年到,也要进行GC,只是GC的频率降低了,如果老年代的对象是垃圾了,使用标记整理的方式释放