目录
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 扫描的频率
上述过程是”分代回收“的基本逻辑
对象 伊甸区 ->生存区 ->生存区 ->老年代 复制算法对象
在老年代中,通过标记-整理(搬运) 来进行回收~~
感谢支持,有帮助点个赞 😜