JVM之内存回收算法

2018-07-12  本文已影响0人  Lebens

概述

GC需要完成3件事情:

那么该如何判断对象已死,可以被回收呢?

引用计数法

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时计数器就-1;任何时候计数器为0时,对象就不再使用,可以被回收。

虽然引用计数法实现简单,效率也很高,但是它很难解决对象之间的相互循环引用问题。

举个例子:

分别new 2个实例对象A以及实例对象B,当A持有B的引用,同时B持有A的引用时。就算A、B不再被使用,如果使用用引用计数法标记对象,A、B都不可能被回收了。

可达性分析算法

可达性分析算法: 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明这个对象是不可用的。

Java中,可作为GC Roots的对象包括下面几种:

对象可达性分析.jpeg

对象的自救

在可达性分析算法中不可达的对象,也并不是“非死不可”,真正宣告一个对象死亡,至少要经历2次标记过程:可达性分析过程中发现没有与GC Roots 相连接的引用链,那它会被标记并且进行一次筛选,筛选的条件是否有必须要执行对象的finalize()方法。当对象没有覆写finalize()或者已经执行过finalize()(也就是finalize()最多执行一次)则虚拟机视为“没有必要执行“。

finalize()是对象自救的最后一次机会,只要重新与GC Roots关联上即可。

对象的引用

JDK1.2之后将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

方法区回收

方法区又称永久代,主要回收2部分内容:废弃的常量和无用的类。

废弃的类回收和堆中对象的回收类似,只要系统没有其他的地方引用都当前字面量,这个常量就可以被回收。

无用的类判断就比较复杂了:

满足以上3个条件这个类可以被回收,但是并不一定回收。

内存回收算法

JVM中常用几种内存回收算法:

标记-清除算法

标记—清除算法分为“标记”和“清除”两个阶段:首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法主要有2个问题:

(图片源自网络)

标记-清除算法.png

复制算法

复制算法:将可用内存空间分为大小相等的2块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,把已使用的内存空间清理掉。

复制算法算法的优点是效率高,不用考虑内存碎片问题,但是代价是内存缩小为了原来的一半。

(图片源自网络)

复制算法.png

新生代一般用此种算法来收集,具体算法以及其中的内存分配担保机制都砸在本文稍后解释。

标记-整理算法

标记-整理算法,标记过程与标记-清除算法一致,只是手续步骤不是直接对可回收对象进行整理,而是让所有的存活对象往一端移动,然后直接清理掉边界以外的内存。

(图片源自网络)

标记-整理算法.png

分代收集算法

这种算法并没有新的思想,只是根据对象存活的周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样就根据各个年代的特点采用最适当的收集算法。

新生代复制算法

新生代所用的复制算法是升级版的复制算法。根据IBM的研究表明,新生代中的对象98%都是“朝生夕死”的,所以并不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,比利是8:1:1.每次使用Eden和其中一块Survivor空间。当发生新生代GC时,将Eden和Survivor中的对象还存活的对象一次性复制到空着的Survivor区中,最后清理掉Eden区和使用中的Survivor区。虽然同事复制算法,但是升级版的复制算法,每次只浪费了10%的内存空间。

我们无法保证每次回收都只有不多余10%的对象存活,当Survivor空间不足时就需要依赖其他内存(老年代)进行分配担保。

内存担保就是当剩余的Survivor空间无法存放上一次GC存活下的对象时,这些对象将直接通过分配担保机制进入老年代。

(图片源自网络)

分代回收一.png 分代回收二.png

上面图片可以看到,对象在Eden和Survivor中来回复制,同时当Survivor空间不足或者对象到一定年龄后将被移动到老年代。

空间分配担保机制

在新生代发生Minor GC前,虚拟机会检测老年代最大可用的连续内存空间是否大于新生代所有的对象的总空间,如果条件成立则Minor GC是安全的。否则判断老年代连续可用内存是否大于历次晋升到老年代对象的平均大小,如果大于则尝试一次Minor GC,小于或者没有设置内存担保机制进行一次Full GC。

所谓的担保就是,当新生代进行Minor GC后仍有大量对象存活的情况下,就需要老年代进行分配担保。所谓的担保就是所有Survivor无法容纳的对象都放入老年代,但是内存回收完成之前无法知道存活的对象数量,就只能按历次晋升到老年的对象平均值作为经验值,从而来决定时候进行Full GC以让老年代腾出更过的空间。担保失败怎还是进Full GC,然后会浪费时间,但是大部分情况下担保都是有效的。

内存分配策略

对象有限在Eden中分配

大多情况下,对象在新生代Eden区中分配。当Eden区中没有足够的空间进行分配的时候,虚拟机将发生一次Minor GC。

大对象直接进入老年代

所谓的大对象指的是需要大量连续空间的Java对象。当对象大于JVM设定的阀值之后直接进入老年代,这样做的目的是避免在Eden区以及两个Survivor区中来回复制对象。

长期存活的对象进入老年代

JVM为每个对象定义了一个对象年龄(Age)计数器。每次Minor GC结束后对象存活则Age +1,当对象的年龄增加到一定程度后(默认15),将会被晋升到老年代。

动态对象年龄判定

当Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,这时年龄大于或等于该年龄的对象就可以直接进入老年代,并不需要年龄增长到阀值。

参考书籍

本文摘录、整理自周志明的《深入理解Java虚拟机》一书,如想获得更详细介绍可自己查阅此书。

上一篇下一篇

猜你喜欢

热点阅读