垃圾收集简称GC。在C/C++中,如果一个变量不再使用,需要程序员手动释放相应的内存,当程序很长,变量很多的时候,这种操作就变得非常繁琐,容易出错。从开发的角度看,程序员也不应该在内存分配释放这样的事情上消耗太多精力。于是有了GC。对于GC我们要理解对什么区域进行GC、对象什么时候要被GC、怎样进行GC。
where?
程序计数器、虚拟机栈、本地方法栈是线程私有的,随线程创建和消亡,栈帧的大小在类结构确定下来时已经确定,这些部分内存的分配和回收具备确定性。而Java堆和方法区不同,一个接口的多个实现类需要的空间大小可能不一样,一个方法的多个分支需要的内存也可能不一样,这些信息只有在运行中才能得知,所以这两个区域内存的分配和回收具有动态性。所以Java的GC器主要面向的是堆和方法区。
when?
引用计数算法
引用计数算法的原理很简单,给对象添加一个引用计数器,每次被引用计数器值+1,引用失效,计数器值-1,一旦计数器值为0,对象就不能再被使用。
引用计数器算法的优点是实现简单,判定效率高,缺点是很难解决对象间循环引用的问题。主流Java虚拟机没有使用引用计数算法。
可达性分析算法
主流的语言都是用可达性分析算法来判定对象是否存活的。原理是以一系列的“GC Roots”对象为起向下搜索,走过的路径称为"引用链",如果从GC Roots到某个对象不可达,这个对象就是不可用的。
GC Roots包括:虚拟机栈->栈帧->本地变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中的Native方法引用的对象。
宣告对象死亡
一个对象在死亡前要经历至少两次标记过程。首先对可达性分析中不可达的对象,会进行第一次标记,然后对这些对象进行筛选,如果对象覆盖了finalize()方法且finalize()方法没有被调用过,会被放入一个叫F-Quene的队列中,在稍后用一个低优先级的Finalizer线程执行。这里的执行只是触发finalize()方法,不能保证等待它运行结束。因为如果一个对象的finalize()方法耗时很长或者发生了死循环,F-Quene中的其他对象就会永远等待,使得内存回收系统崩溃。短暂等待后,GC会对F-Quene中的对象进行第二次标记。
如果一个不可达的对象想免于死亡,唯一的自救方法就是在finalize()方法中重新与引用链上的对象建立关联。这时候GC会把这样的对象移出“即将回收”的集合。
然后,剩下的对象会被回收。
方法区的回收
方法区(HotSpot中的永久代)主要回收废弃常量和无用的类。
废弃常量:如果一个常量在发生内存回收时没有被任何对象引用,就是废弃常量。
无用的类:满足三个条件 1.Java堆中不存在这个类的实例;2.该类的加载器已经回收;3.该类对应的Class对象没有被任何地方引用。满足以上三条,标记“可以回收”。
How?
标记-清除算法
包括“标记”“清除”两个阶段。先标记,再清除。是最基础的收集算法。
有两个主要缺点:
1. 标记和清除的效率都不高;
2.标记-清除之后会产生大量的内存碎片,可能导致在以后想要分配较大空间时找不到足够的连续内存而提前触发另一次垃圾收集动作。
复制算法
把可用内存分成大小相等的两块,每次使用一块,当一块内存用完,把存活的对象复制到另一块上,再把第一块的内存空间直接清理掉。这样再进行内存分配就不用考虑内存碎片了,直接移动堆顶指针按顺序分配就可以。缺点是把内存缩小为一半,代价太大。
现在的商业虚拟机在新生代使用了类似的方法,但是在空间效率上进行了改进。IBM的研究表明,新生代绝大部分对象是“朝生夕死”的,存活时间很短,所以不用按照1:1来划分空间,而是将内存分为一个大的Eden区和两个小的Survivor区。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。在进入老年代之前,存活的对象会在两个Survivor区之间来回复制。HotSpot默认Eden和Survivor空间大小8:1,这样一个时刻只有10%的空间是保留的,空间效率大幅提升。
但是我们并不能保证每次回收存活的对象都不超过10%,所以当Survivor空间不够用,就要用老年代的内存进行分配担保。如果另一块Survivor放不下上一次新生代中存活的对象,这些对象会直接进入老年代。
标记-整理算法
如果对象存活率很高,复制算法就要进行很多的复制操作,效率变低,另一方面还需要分配担保,所以在老年代不能使用复制算法。在老年代一般使用标记-整理算法。
标记整理算法的“标记”过程和标记-清除算法一样,但是接下来不是直接进行清除,而是让所有存活的对象向一端移动,然后直接清理掉边界外的内存。这样清理之后,可用内存是连续的,可以方便地进行分配。
分代收集
当代商业虚拟机使用“分代收集”的策略,根据对象存活周期不同把内存分为几块,一般实把Java堆分为新生代和老年代,根据不同代的特点选择适当的收集算法。
在新生代,每次垃圾收集都有大量对象死去,只有少量存活,使用复制算法。
在老年代,对象存活率高,且没有空间为它进行分配担保,只能使用“标记-清除”或者“标记-整理”算法。