【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扫描的频率

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

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

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


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

相关推荐
学到头秃的suhian13 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN20 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海1 天前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗1 天前
JVM整理
jvm
echoyu.1 天前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考2 天前
JVM中内存管理的策略
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z2 天前
【JVM】详解 线程与协程
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm