GC与内存分配策略
GC需要完成三件事:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
对象已死吗
引用计数法
这个算法是这样判断对象是否存活的:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
它无法解决对象之间循环引用的问题,同时虚拟机也不是通过引用计数法来判断对象是否存活的
简单示例: 对象objA和objB都有属性instance,赋值令objA.instance = objB 及 objB.instance = objA, 除此之外,这两个对象再无其他引用,实际上着两个对象已经不可能再被访问,但是他们因为互相引用而导致对方的引用计数都不为0,于是引用技术算法无法通知GC收集器去回收它们。
可达性分析
以GC Roots
的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots
没有任何引用链相连(图论中即为从GC Roots
到这个对象不可达)时,则证明对象是不可用的。
Java中,可作为GC Roots
的对象包括以下:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
引用的类型
无论是通过引用计数法去判断对象的引用数量,还是通过可达性分析算法去判断对象的引用链是否可达,这些都与“引用”有关。 Java中的引用有四种:
-
强引用(StrongReference)
在代码中普遍存在,类似 Object obj = new Object()这类引用; 只要强引用还存在,垃圾收集器永远不会回收被它引用的对象。
-
软引用 (SoftReference)
有用但非必要的对象,“食之无味,弃之可惜”。在内存空间还足够时,则能保存在内存之中,如果内存空间在垃圾收集后还是非常紧张,则可以抛弃这些对象。
-
弱引用 (WeakReference)
非必需的对象,被它引用的对象只能生存到下一次垃圾收集发生之前。
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
-
虚引用 (PhantomReference)
最弱的一种引用关系,随时都可能被回收,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的时,在这个对象被收集器回收时收到一个系统通知。
垃圾收集算法
标记-清除算法(Mark-Sweep)
该算法分为“标记”和“清除”两个阶段: 首先需要标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
问题: 会产生大量的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾收集动作。
复制算法
问了解决大量的内存碎片问题,复制算法就出现了。它将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
问题:
- 直接将内存缩小了一半
- 在对象存活率较高情况下,复制的代价比较大
现在很多虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间。而是将内存划分为一块大的Eden区和两块较小的Survivor区,比例为8:1:1。也就是说新生代中可用内存空间变成总容量的90%, 剩下的10%的空间用于放置每次存活的对象。但我们没有办法保证,每次存活的对象比例都小于10%,因此当Survivor空间不够用时,需要依赖其他内存进行分配担保。
标记-整理算法
先标记出需要回收的对象,之后将存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
将Java堆分成新生代和老年代。
新生代: 大批对象死去,少量对象存活,可采用复制算法。(只需要付出少量存活对象的复制成本就可以完成收集)
老年代: 少量对象死去,大批对象存活,采用标记-清除或标记整理算法。(没有额外的空间对它进行担保)