JVM的垃圾回收机制

目录

GC的工作范围

谁是垃圾

怎么判断,某个对象是否有引用指向捏?

(1)引用计数

缺陷

释放垃圾的策略

(1)标记清除(不实用)

(2)复制算法

(3)标记整理

分代回收(中和方案)


垃圾回收机制GC是java提供的对于内存自动回收的机制,相对于C/C++的手动回收是方便不少的,但是一切的方便都是需要代价的,GC需要消耗额外的系统资源,而且存在非常影响执行效率的STW(stop the world)问题,触发GC的时候,就可能一瞬间把系统负载拉满,这些是C/C++无法容忍的。

GC的工作范围

GC回收的是”内存“,更准确的说是”对象“,回收的是”堆上的内存“。

(1)程序计数器:不需要额外回收,线程销毁,自然就回收了

(2)栈:不需要额外回收,线程销毁,自然回收了

(3)元数据区:一般也不需要,都是加载类,很少卸载类

(4)堆:GC的主要工作区

谁是垃圾

GC是自动回收的,那么回收怎么知道这个对象是垃圾捏?

一个对象,什么时候创建,时机往往是明确的。 但是什么时候不再使用, 时机往往是模糊的。在编程中,一定要确保,代码中使用的每个对象,都得是有效的,,万不要出现"提前释放"的情况
宁可放过,也不能错杀~~
因此判定一个对象是否是垃圾,判定方式是比较保守的~~

此处引入了非常"保守"的做法,一定不会误判的做法(可能会释放的不及时),判定某个对象,是否存在引用指向它

例如:
Test t = new Test();
t=null;

使用对象,都是通过引用的方式来使用的,如果没有引用指向这个对象,意味着这个对象注定无法再代码中被使用。将t指向为null,此时new Test()的对象就没有引用指向了,此时这个对象就可以认为是垃圾了。

怎么判断,某个对象是否有引用指向捏?

介绍两种方法

(1)引用计数

为对象的本体加一个计数器,对象每被引用一次,计数器就+1,对象每少一个引用,计数器就-1,当计数器为0时,此时对象就是垃圾了

缺陷

(1)消耗额外的内存空间:如果你的对象比较大,浪费的空间还好(有1w块钱,花1块钱在计数器上就还行),对象比较小,空间占用就多了(只有10块钱,花一块钱在计数器上),并且对象数目越多,空间浪费的就多

(2)存在”循环引用“的问题,代码解释

public class A {
    private B b;

    public void setB(B b) {
        this.b = b;
    }
}

public class B {
    private A a;

    public void setA(A a) {
        this.a = a;
    }
}

public class Main {
    public static void main(String[] args) {
        A objA = new A();    //A的计数器+1
        B objB = new B();    //B的计数器+1

        objA.setB(objB);    //A中引用了B,B的计数器再+1
        objB.setA(objA);    //B中引用了A,A的计数器再+1

        objA=null;        //A的计数器-1
        objB=null;        //B的计数器-1

    }
}

在上述情况下,即使没有任何其他对象引用A和B,它们的引用计数也永远不会变为零,因为它们之间互相引用,计数器都为1,判定不是垃圾,导致无法释放内存,但是外部代码也无法访问到这两对象!!!

注意:引用计数不是JVM采取的方案,而是Python/PHP的方案

(2)可达性分析(是JVM采取的方案)

这个方案解决了空间上的问题,也解决了循环引用的问题,但是也需要付出代价,时间上的代价。

JVM把对象之间的引用关系,理解成一共”树形结构“,JVM会不停的遍历这样的结构,把所有能够遍历访问到的对象标记成”可达“,剩下的就是”不可达“

举例:简单的伪代码帮助理解

class Node{
    Node left;
    Node right;
}
Node build(){
    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 = build();    //此处只有一共引用,通过这个引用就能访问到树上所有节点对象

上述代码的树状图如下(简画)

a的左边是b,右边是c,b的左边是d,右边是e,c的右边是f,e的右边是g

在上述树状图中,如果a.right=null,此时c就不可达了,同时f也不可达了,访问不到就标记成垃圾被回收,如果写了root=null,就是要把树上所有的对象都干掉

java代码中可能会有很多棵这样的树,JVM就会周期性的堆这所有的树进行遍历,不停的标记可达,也不停的把不可达的对象干掉

释放垃圾的策略

(1)标记清除(不实用)

直接把标记为垃圾的对象对应的内存释放掉,简单粗暴

这样的做法会存在"内存碎片"问题,空闲内存被分成一个个的碎片了,后续很难申请到大的内存!

(2)复制算法

比如上图,要释放 1,3,5, 保留 2,4,不会直接释放1,3,5 的内存,而是把2,4拷贝到另外一块空间中,让后将原来的1,2,3,4,5的空间全部释放,这样就解决了内存碎片问题

缺点:浪费空间太多,如果要保留的空间比较多,复制的时间开销也不少

(3)标记整理

标记整理算法首先从一组根对象出发,通过可达性分析,标记所有活动对象。在标记完成后,所有被标记的存活对象会被移动到内存空间的一端,形成一个紧凑的连续区域,而未被标记的对象则被视为垃圾对象。然后,垃圾对象所占用的内存空间会被释放出来,形成一个连续的、空闲的内存区域。最后,整个内存空间会被整理,将存活对象移动到一端,释放的空闲内存空间集中到另一端,从而减少内存碎片化,提高内存的利用率

类似于顺序表中删除中间元素

上述三种方案只是铺垫,JVM中实际的方案,是综合上述的方案,分代回收

分代回收(中和方案)

分情况讨论,根据不同的场景/特点,选择合适的方案~~

根据对象的年龄来讨论的,GC 有一组线程,周期性扫描。某个对象经历了一轮 GC 之后,还是存在,没有成为垃圾,年龄 +1。
可以这么理解:"要g 早g 了"既然没有早g,说明这个对象,有东西,还能继续存在!!!

把新创建的对象,放到伊甸区中,
伊甸区中,大部分的对象,生命周期都是比较短的,第一轮 GC 到达的时候,就会成为垃圾只有少数对象能活过第一轮 GC 

伊甸区 ->生存区
通过复制算法。(由于存活对象很少,复制开销也很低,生存区空间也不必很大)

生存区 ->另一个生存区
通过复制算法,没经过一轮 GC,生存区中都会淘汰掉一批对象,剩下的通过复制算法,进入到另一个生存区(进入另一个生存区的还有从伊甸区进来的对象)
存活下来的对象, 年龄 +1

生存区 ->老年区        某些对象,经历了很多轮 GC,都没有成为垃圾,就会复制到老年代。老年代的对象,也是需要进行 GC 的,但是老年代的对象生命周期都比较常, 就可以降低 GC 扫描的频率

上述过程是”分代回收“的基本逻辑

对象 伊甸区 ->生存区 ->生存区 ->老年代 复制算法对象

在老年代中,通过标记-整理(搬运) 来进行回收~~

感谢支持,有帮助点个赞 😜

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值