本文将分析JVM的垃圾回收策略,哪些内存需要回收和如何回收的问题。
哪些内存需要回收
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还存活着,哪些已经死去。
引用计数算法
引用计数算法是指,在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的缺陷是不能解决对象之间互相引用的问题,因此不被主流虚拟机选用。
可达性分析算法
通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,这个对象就是不可用的。
在java语言中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
引用的分类
java的引用可以分为强引用、软引用、弱引用、虚引用:
- 强引用:是指在程序代码中直接存在的引用,类似“Object obj = new Object()”这类的引用。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:还有用但是并非必需的引用,在系统将要发生内存溢出异常之前会把这些对象列进回收范围中进行二次回收,若还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用:非必需的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否够用都将回收这些对象。
-
虚引用:最弱的一种引用关系。一个对象是否有虚引用的存在完全不会对他的生存时间构成影响,也无法通过虚引用来取得一个对象实例。
垃圾收集算法
标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为标记和清除两个阶段。
标记:首先标记所有需要回收的对象
清除:在标记完成后统一回收所有被标记的对象
缺点:
- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。
复制算法(新生代算法)
它将可用内存按容量划分为大小相等的两块,每次只用其中的一块。当这一块内存用完之后,将还存活的对象复制到另一块去,然后再把已使用过的内存空间一次清理掉。
优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:代价是将内存缩小为了原来的一半,未免太高了一点。
现在的商用虚拟机都采用这种手机算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”,所以并不需要按照1:1的比例来划分内存空间。
解决方法:将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
标记-整理算法(老年代算法)
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。所以在老年代一般不能直接选用这种算法。根据老年代的特点,提出了“标记-整理”(Mark-Compact)算法。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把Java堆分为新生代和老生代,根据各个年代采用适当的收集算法。
- 新生代一般采用复制算法(Copying)。
- 老生代一般采用 标记-清理(Mark-Sweep) 或者标记-整理(Mark-Compact) 进行回收。