【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.找到谁是垃圾
谁是垃圾,这个事情,不太好找;一个对象,什么时候创建,时机往往是明确的,但是什么时候不在使用,时机往往是模糊的...在编程中,一定要确保,代码中使用的每个对象,都得是有效的,千万不要出现“提前释放”的情况(宁可放过,也不能错杀)
因此判定一个对象是否是垃圾,判定方式是比较保守的;此处引入了一个非常"保守“的做法,一定不会误判的做法(可能会释放不及时),判定某个对象,是否存在引用指向它
Test t = new Test();
使用对象,都是通过引用的方式来使用的,如果没有引用指向这个对象,意味着这个对象注定无法在代码中使用,如果没有引用指向的对象就可以视为是垃圾了
Test t = new Test();
t = null;
修改t的指向,此时new Test()的对象就没有引用指向了,此时这个对象就可以认为是”垃圾“
具体怎么判断,某个对象是否有引用指向呢?有很多方式来实现,下面介绍两种方式
如果面试官问:GC中,如何判定对象是垃圾,那么两种都要回答
如果问:Java的GC中,如何判定对象是垃圾,那么只需要回答第二种
1)引用计数(不是JVM采取的方式,而是Python/PHP的方案)
给对象增加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器就+1;当引⽤失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使⽤的,即对象已"死"
Test a = new Test();
Test b = a;
//把对象的地址进行赋值,都知道对象的地址了,自然就能找到对象,更能找到对象旁边的计数器了
b = null;
a = null;
//引用计数器为0,此时对象就是垃圾了
but,这种方案有两个缺陷:
(1)消耗额外的存储空间
如果对象比较大,浪费的空间还好,对象比较小,空间占用就多了,并且对象数目多,空间的浪费就多了
(2)存在”循环引用“的问题(面试考到引用计数的重点)
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就会不停的遍历这样的结构,把所有能够遍历访问到的对象标记成”可达“,剩下的就是”不可达“
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扫描的频率
综上就是”分代回收“的基本逻辑
对象 伊甸区-> 生存区-> 生存区 ->老年区 复制算法
对象在 老年代中,通过标记-整理(搬运)来进行回收
垃圾回收还有一个话题:垃圾回收器,上述思想的具体实现,实现过程有很多优化和策略的体现