java JVM - 垃圾回收机制

垃圾的定义:只要对象不再被使用了,那我们就认为该对象就是垃圾,对象所占用的空间就可以被回收

怎么判断对象不再被使用的呢?
常用的算法有两个「引用计数法」和「可达性分析法」

引用计数法思路很简单:当对象被引用则+1,引用释放时计数则-1。当计数器为0时,说明对象不再被引用,可以被可回收
引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A);

另一种就是可达性分析法:它从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收。

GC Roots 的对象有:
虚拟机栈(栈桢中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的native方法)引用的对象

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize方法
当对象变成GC Roots不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象复活。
每个对象只能触发一次finalize方法
由于finalize方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用。

垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」引用的对象
在这里插入图片描述
候选者:标记完之后,我们就可以选择直接「清除」,只要不被「GC Roots」关联的,都可以干掉

候选者:过程非常简单粗暴,但也存在很明显的问题

候选者:直接清除会有「内存碎片」的问题:可能我有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)
在这里插入图片描述
候选者:那解决「内存碎片」的问题也比较简单粗暴,「标记」完,不直接「清除」。

候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了

候选者:这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去

面试官:嗯…

候选者:还有一种「折中」的办法,我未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动

候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛

候选者:这种专业的术语就叫做「整理」
在这里插入图片描述
候选者:扯了这么久,我们把思维再次回到「堆」中吧

候选者:经过研究表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间

候选者:又由于「垃圾回收」是会导致「stop the world」(应用停止访问)

候选者:理解「stop the world」应该很简单吧:回收垃圾的时候,程序是有短暂的时间不能正常继续运作啊。不然JVM在回收的时候,用户线程还继续分配修改引用,JVM怎么搞(:

候选者:为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率

候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行区分,死得快的对象所占的区域叫做「年轻代」,活得久的对象所占的区域叫做「老年代」
在这里插入图片描述
候选者:但也不是所有的「垃圾收集器」都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。

候选者:所以,你可以看到我的「堆」是画了「年轻代」和「老年代」

候选者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是没有分代的概念的(:

候选者:只不过我为了好说明现状,ZGC的话有空我们再聊

面试官:嗯…好吧

候选者:在前面更前面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,分别是:

候选者:标记清除算法、标记复制算法和标记整理算法【「标记」「清除」「复制」「整理」】

候选者:经过上面的铺垫之后,这几种算法应该还是比较好理解的
在这里插入图片描述
候选者:「分代」和「垃圾回收算法」都搞明白了之后,我们就可以看下在JDK8生产环境及以下常见的垃圾回收器了

候选者:「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew

候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS

候选者:看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程

候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)

候选者:CMS是「JDK8之前」是比较新的垃圾收集器,它的特点是能够尽可能减少「stop the world」时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!
在这里插入图片描述
候选者:又可以发现的是,「年轻代」的垃圾收集器使用的都是「标记复制算法」

候选者:所以在「堆内存」划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动)

候选者:而新的对象则放入Eden区

候选者:我下面重新画下「堆内存」的图,因为它们的大小是有默认的比例的
在这里插入图片描述
候选者:图我已经画好了,应该就不用我再说明了

面试官:我还想问问,就是,新创建的对象一般是在「新生代」嘛,那在什么时候会到「老年代」中呢?

候选者:嗯,我认为简单可以分为两种情况:

候选者:1. 如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)

候选者:2. 如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)
在这里插入图片描述
面试官:既然你又提到了Minor GC,那Minor GC 什么时候会触发呢?

候选者:当Eden区空间不足时,就会触发Minor GC

面试官:Minor GC 在我的理解就是「年轻代」的GC,你前面又提到了「GC Roots」嘛

面试官:那在「年轻代」GC的时候,从GC Roots出发,那不也会扫描到「老年代」的对象吗?那那那…不就相当于全堆扫描吗?

候选者:这JVM里也有解决办法的。

候选者:HotSpot 虚拟机「老的GC」(G1以下)是要求整个GC堆在连续的地址空间上。

候选者:所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过「地址」就可以判断对象在哪个分代上

候选者:当做Minor GC的时候,从GC Roots出发,如果发现「老年代」的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)在这里插入图片描述
面试官:但又有个问题,那如果「年轻代」的对象被「老年代」引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉「年轻代」的对象的。

候选者:HotSpot虚拟机下 有「card table」(卡表)来避免全局扫描「老年代」对象

候选者:「堆内存」的每一小块区域形成「卡页」,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为「脏页」

候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值