垃圾回收机制(GC)

一、为什么要有垃圾回收机制?

我们由C语言中的动态内存管理来引入,malloc申请内存,free释放内存

此处用 malloc 申请到的内存,生命周期是跟随整个进程的。这一点对于服务器程序非常不友好。服务器每个请求都去 malloc 一块内存,如果不 free 释放,就会使申请到的内存越来越多,后续想要申请内存就没有内存可申请了,这就是内存泄漏问题。

实际开发中,很容易出现 free 不小心就忘记调用了,或者因为一些情况没有执行到(函数中间存在 if -> return 或者 抛出异常了)。

那能否让释放内存的操作,由程序自动负责完成呢?

java 就属于早期就支持 垃圾回收 的语言了。引入垃圾回收机制之后,就不需要靠手动来释放内存了,程序会自动判定某个内存是否会继续使用,如果后续不再使用了,就会自动释放掉。

二、垃圾回收的主要内存区域

垃圾回收,就是回收内存 。

我们之前有说到内存区域划分,JVM中的内存分为好几块:

  1. 程序计数器:不需要垃圾回收(GC)
  2. 栈:不需要GC,局部变量都是在代码块执行结束后自动销毁(栈自身的特点,和垃圾回收没关系)
  3. 元数据区/方法区:一般不需要GC
  4. 堆:GC的主要“战场”

所以说,垃圾回收 回收的主要是 堆 这块内存区域。

三、垃圾回收过程

垃圾回收需要:

  1. 识别出垃圾:哪些对象是垃圾(不再使用),哪些对象不是垃圾
  2. 把标记为垃圾的对象的内存空间进行释放

1、识别出垃圾

 判定这个对象后续是否还要继续使用。

在Java中,使用对象一定是要通过引用的方式来使用(例外,匿名对象,如 new MyThread().start(); 但是,当这行代码执行完,对应的MyThread对象就会被当做垃圾)。

如果一个对象没有任何引用指向它,就视为是无法被代码中使用,就可以作为垃圾了。

举例:

void func() {
    Test t = new Test();
}

 

上述代码通过 new Test()上创建了对象,存储在 中的局部变量 t 保存了对象的地址。当代码执行到最后的 } 之后,局部变量 t 就直接释放了,上述 new Test() 对象也就没有引用再指向它,此时,这个代码就无法访问使用这个对象了,该对象就是垃圾了。

如果代码更复杂些,如:

Test t1 = new Test();
Test t2 = t1;
Test t3 = t2;
Test t4 = t3;

此时就会有很多的引用指向new Test()对象。此时通过任意的引用都能访问到该对象,需要确保所有的指向该对象的引用都销毁了,才能把 Test 对象是为垃圾。

 1.1 引用计数算法

这种思想方法,给每个对象安排一个额外的空间,空间里要保存当前这个对象有几个引用。此时垃圾回收机制,有专门的扫描线程,去获取到当前每个对象的引用计数情况,发现对象的引用计数为0,说明这个对象就可以被视为垃圾,进行释放了。

例子:

Test a = new Test();
Test b = a;

a = null;
b = null;

当执行完这4行代码,堆栈的情况如下:

此时 Test 对象的引用计数为 0,就可被视为垃圾。

引用计数算法是一个简单有效的算法,但存在两个关键问题:

问题一:消耗额外的内存空间

要给每个对象都安排一个计数器。如果计数器按照 2 个字节来算,整个程序中的对象数目很多的话,总的消耗的空间也会非常多。 尤其是如果每个对象的体积比较小(假设每个对象 4 个字节),那么此时计数器消耗的空间,已经达到了对象的空间的一半。

问题二:引用计数可能会产生 “循环引用” 问题,此时引用计数算法就无法正确工作了

 如下代码:

class Test{
    Test t;
}

Test a = new Test();
Test b = new Test();

a.t = b;
b.t = a;

a = null;
b = null;

执行完上述几行代码之后,堆栈情况:

 可见两个对象的引用计数都不为0,不能被垃圾回收机制回收掉!但是这两个对象又无法使用了。这就是引用计数存在的问题。

1.2 可达性分析算法

JVM自身知道一共有哪些对象,通过可达性分析的遍历(JVM中存在扫描线程,会不停地尝试对代码中已有的这些变量,进行遍历,尽可能多的去访问到对象),把可达的对象都标记出来,剩下的自然就是不可达的。

我们通过构造一个二叉树来理解:

class Node{
    char val;
    Node left;
    Node right;
}

Node buildTree() {
    Node a = new Node();
    Node b = new Node();
    Node c = new Node();
    Node d = new Node();
    Node e = new Node();
    Node f = new Node();
    Node g = new Node();
    a.left = b;
    a.right = c;
    b.left = d;
    b.right = e;
    e.left = g;
    c.right = f;
    return a;
}

Node root = bulidTree();

这个代码中只有一个 root 这样的引用,但实际上上述七个节点对象都是“可达”的。

 

上述代码,如果执行这个代码:root. right.right = null; 此时 f 这个对象就被“孤立”了。按照上述从 root 出发进行遍历的操作,就无法访问到 f 了,f 这个节点就是“不可达”的了。

如果代码中出现: root.right = null; 此时 c 就不可达了,而 f 的访问必须通过 c ,因此 c 不可达也导致了 f 的不可达,此时节点 c 和 f 都是垃圾了。

2、 垃圾回收算法

通过上面介绍,我们已经知道了如何判断出对象是否为垃圾,下面我们就来看看识别出这些垃圾之后,如何回收它们( 将垃圾对象的内存空间进行释放)。

2.1 标记-清除算法

"标记-清除" 算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收 的对象,在标记完成后统⼀回收所有被标记的对象。
"标记-清除"算法的不足主要有两个
  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

2.2 复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉

这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。

复制算法的不足之处:

  1. 总的可用内存变少了,因为将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
  2. 如果每次要复制的对象比较多,此时复制开销也就大了

 对于大部分对象需要释放,少数对象存活的情况适合复制算法。

2.3 标记-整理算法

" 标记" 过程仍与"标记-清除"过程⼀致, 但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。该算法搬运内存开销会很大。

 这种思想类似于顺序表删除中间元素,后面要进行搬运。

 

 

 2.4 分代回收算法

分代算法和上面的 3 种算法不同, 分代算法是通过区域划分,实现不同区域和不同的垃圾回收策
略,从而实现更好的垃圾回收 。这就好比中国的一国两制方针一样,对于不同的情况和地域设置更符 合当地的规则,从而实现更好的管理,这就是分代算法的设计思想。
当前 JVM 垃圾收集都采用的是"分代回收"算法,这个算法并没有新思想, 只是根据对象存活周期的不同将内存划分为几块 。⼀般是把Java堆分为 新生代和老年代 。在新生代 中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活 率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
哪些对象会进入新生代?哪些对象会进入老年代?
  • 新生代:⼀般创建的对象都会进入新生代;
  • 年代:大对象和经历了 N 次(⼀般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。

下面我们引入对象的年龄,来理解分代算法: JVM中有专门的线程负责周期性扫描,一个对象如果被线程扫描了一次,就说明是可达的,年龄就加 1 (初始年龄为0)。JVM根据对象年龄的差异,把整个堆内存分为两大部分:新生代和老年代。

 

  1. 当代码中 new 出来一个新的对象,该对象就是被创建在伊甸区的,伊甸区中就会有很多对象。 伊甸区中的对象大部分是活不过第一轮GC的,它们的生命周期非常短。
  2. 第一轮 GC 扫描完成之后,少数伊甸区中幸存的对象,就会通过复制算法,拷贝到生存区。后续 GC 的扫描线程还会持续进行扫描,不仅要扫描伊甸区的对象,还要扫描生存区对象。生存区中的大部分对象也会在扫描中被标记为垃圾,少数存活的,就会继续用复制算法,拷贝到另一个生存区。每经过一轮GC 扫描,对象的年龄都会加 1 。
  3. 如果这个对象在生存区中,经历了若干轮 GC ,仍然健在,JVM 就会认为这个对象的生命周期大概率很长,就把这个对象从生存区拷贝到老年代了。
  4. 老年代的对象也要被 GC 进行扫描,但是扫描的频次就大大降低了。
  5. 对象在老年代成为垃圾之后,JVM 就会按照标记-清除 或 标记-整理的方式释放内存。
Minor GC和Full GC,这两种GC有什么不⼀样?
  • Minor GC,又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,⼀般回收速度也比较快。
  • Full GC,又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度⼀般会比Minor GC慢10倍以上

四、垃圾回收器

垃圾收集器就是内存回收的具体实现。 垃圾回收算法是垃圾收集器的指导 思想。
 
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的⼀种技术,它是将程序中不用 的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用 。所处的区域,表示它是属于新生代收集器还是老年代收集器。

这里对于垃圾收集器就不多介绍了。

 

  • 24
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值