【JVM】垃圾回收机制|死亡对象的判断算法|垃圾回收算法

思维导图

目录

1.找到谁是垃圾

1)引用计数(不是JVM采取的方式,而是Python/PHP的方案)

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

2.释放对应的内存的策略

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

2)复制算法

3)标记-整理

4)分代算法


垃圾回收机制GC,是Java提供的对于内存自动回收的机制,相对于C/C++的手动回收malloc free来命名的,GC需要消耗额外的系统资源,而且存在非常影响执行效率的"STW"问题(stop the world),触发GC的时候,就可能一瞬间把系统负载拉满,导致服务器无法响应其他的请求

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

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

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

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

4)堆:GC的主力

回收一定是一次回收一个完整的对象,不能回收半个对象(一个对象有10个成员,肯定是把10个成员的内存都回收了,而不是只回收一部分)

GC的流程,主要是两个步骤:

1.找到谁是垃圾(即不被继续使用的对象)

2.释放对应的内存


1.找到谁是垃圾

谁是垃圾,这个事情,不太好找;一个对象,什么时候创建,时机往往是明确的,但是什么时候不在使用,时机往往是模糊的...在编程中,一定要确保,代码中使用的每个对象,都得是有效的,千万不要出现"提前释放"的情况(宁可放过,也不能错杀)

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

java 复制代码
Test t = new Test();

使用对象,都是通过引用的方式来使用的,如果没有引用指向这个对象,意味着这个对象注定无法在代码中使用,如果没有引用指向的对象就可以视为是垃圾了

java 复制代码
Test t = new Test();
t = null;

修改t的指向,此时new Test()的对象就没有引用指向了,此时这个对象就可以认为是"垃圾"

具体怎么判断,某个对象是否有引用指向呢?有很多方式来实现,下面介绍两种方式

如果面试官问:GC中,如何判定对象是垃圾,那么两种都要回答

如果问:Java的GC中,如何判定对象是垃圾,那么只需要回答第二种


1)引用计数(不是JVM采取的方式,而是Python/PHP的方案)

给对象增加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器就+1;当引⽤失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使⽤的,即对象已"死"

java 复制代码
Test a = new Test();
Test b = a;
//把对象的地址进行赋值,都知道对象的地址了,自然就能找到对象,更能找到对象旁边的计数器了
b = null;
a = null;
//引用计数器为0,此时对象就是垃圾了

but,这种方案有两个缺陷:

(1)消耗额外的存储空间

如果对象比较大,浪费的空间还好,对象比较小,空间占用就多了,并且对象数目多,空间的浪费就多了

(2)存在"循环引用"的问题(面试考到引用计数的重点)

java 复制代码
class Test {
    Test t;
}

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

a = null;
b = null;

此时,这两对象相互指向对方,导致两个对象的引用计数都为1(不为0,不是垃圾),但是外部代码,也无法访问到这两个对象

这个问题也是能解决的,需要付出额外的代价(搭配一些其他的机制来解决),Python和PHP通过额外的机制,避免形成循环引用(自动检测是否形成了循环引用,并且给出了一些报错提示)


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

解决了空间的问题,也解决了循环引用的问题,但是同时也要付出代价,时间上的代价

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

java 复制代码
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.right = null;

此时c就不可达了,同时f也就不可达了

GC roots(这些数的根节点是怎么确定的?)

Java代码中,你所有的

1)栈上的局部对象,引用类型的,就是GC roots

2)常量池中,引用的对象

3)方法区中的静态成员

很多个上述的树,JVM就会周期性的对这所有的树进行遍历,不停的标记可达,也不停的把不可达的对象干掉;具体树是否复杂,都取决于实际代码的实现(有时会很复杂)

but:由于可达性分析,需要消耗一定的时间,因此Java垃圾回收,没法做到"实时性",周期性进行扫描(JVM提供了一组专门的负责GC的线程,不停的进行扫描工作)


2.释放对应的内存的策略

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

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

白色区域:正在使用的

黑色区域:已经释放的,其他代码可以重复利用的

但是这样的做法会存在"内存碎片"问题,空闲内存被分为一个个的碎片了,后续很难申请到大的内存。申请内存,都是要申请"连续"的内存空间的


2)复制算法

要释放1,3,5,保留2,4,不会直接释放1,3,5的内存,而是把2,4拷贝到另一块空间中

内存碎片的问题就解决了,但是这样空间浪费太多,如果你要保留的对象比较多,复制的时间开销也不小


3)标记-整理

释放2,4,6,保留1,3,5,7

类似于"顺序表删除中间元素",搬运

这样能解决内存碎片,也能解决空间利用率的问题

但是这样的搬运,时间开销更大


4)分代算法

上述三个方案,只是铺垫,JVM中的实际方案,是综合上述的方案,更复杂的策略,分代回收(即分情况讨论,根据不同的场景/特点选择合适的方案)

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

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

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

生存区->老年代 某些对象,经历了很多轮GC,都没有成为垃圾,就会复制到老年代

老年代的对象,也是需要进行GC的,但是老年代的对象生命周期都比较长,就可以降低GC扫描的频率

综上就是"分代回收"的基本逻辑

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

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


垃圾回收还有一个话题:垃圾回收器,上述思想的具体实现,实现过程有很多优化和策略的体现

相关推荐
东阳马生架构16 小时前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥1 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇1 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm
程序员志哥1 天前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
聊聊volatile的实现原理?
java·jvm·redis
_LiuYan_1 天前
JVM执行引擎JIT深度剖析
java·jvm
王佑辉1 天前
【jvm】内存泄漏的8种情况
jvm
工业甲酰苯胺1 天前
JVM简介—1.Java内存区域
java·jvm·python
yuanbenshidiaos2 天前
c++---------数据类型
java·jvm·c++