JVM垃圾收集器基本思想
要做JVM内存垃圾回收首先要明确两个问题:
- 哪些内存需要回收
- 怎么回收
- 什么时候回收
1.哪些内存需要回收
jvm内存可以分为两类:
- 线程独占内存区域(程序计数器、虚拟机栈、本地方法栈)
- 线程共享区域(堆、方法区)
对于线程独占内存区域来说,他们的回收时机是非常确定的,在方法结束或线程结束的时候回收即可。
对于线程共享区域里的堆和方法区两个区域,都可以进行垃圾收集,但是方法区的收集效率远低于堆,因此java虚拟机规范中不要求虚拟机必须实现方法区的垃圾收集。我们下文中所指的垃圾收集如无特殊说明,都是针对线程共享区域里的堆进行垃圾收集。
既然垃圾收集主要是针对堆进行的,而堆的作用就是存储对象。我们知道,虽然对象存储在堆里,但是如果要真正的使用对象,要通过对象的引用,因此,当一个对象没有引用指向它的时候,我们就可以回收它了。
1.1引用计数算法
给对象添加一个引用计数器,每次被引用就+1,每次有引用失效时,就-1。当对象的引用数量为0时代表对象不可再被使用。
问题:当有对象互相引用的时候,如果这两个互相引用的对象本身都已经不可能再被访问了,但是因为相互引用的关系,他们的引用计数都不是0,也无法被回收。
public class Ref { public Object instance; public static void main(String[] args) { Ref a = new Ref(); Ref b = new Ref(); a.instance = b; b.instance = a; a = null; b = null; System.gc();// 此时垃圾回收无法回收a、b } }
1.2可达性分析算法
目前的主流实现。
基本思想是通过一系列称为"GC Roots"的对象作为起始点向下搜索,搜索走过的路径称为引用链,没有引用链到达的对象视为不可再被使用。
可视为“GC Roots”的对象包括:
- 虚拟机栈中的引用对象
- 本地方法栈中的引用对象
- 方法区中静态属性引用对象、常量引用对象
2.如何回收
垃圾收集算法有以下这些:
标记-清除算法
算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收。因为被“清除”的对象的内存空间是不连续的,因此清除后会产生大量不连续的内存碎片,内存碎片过多会导致需要分配大对象时找不到连续内存从而触发另一次垃圾回收。
复制算法
将内存划分为容量相等的两块,每次只使用一块,当进行垃圾回收的时候,只将存活的对象复制到另一块内存上,然后将之前的一块内存直接清除。缺点是浪费内存。
现代的商业虚拟机都是使用复制算法来回收新生代的,因为新生代中的对象98%都是朝生夕死,因此不需要按照1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还活着的对象一次性复制到另一块Survivor上,最后清理掉Eden和刚才用过的Survivor空间。
标记-整理算法
和标记-清除算法类似,标记阶段是一样的,只是后续不直接清除可回收对象,而是让所有存活的对象向一端移动,然后清除掉端边界以外的内存。
分代收集算法
这个算法并没有什么新的思想,只是根据对象存活周期将内存划分为几块,每块根据存活周期选择最合适的算法。一般是分为新生代和老年代。新生代每次垃圾收集都只有少量对象存活,可以选择复制算法;老年代因为对象存活率高,没有额外空间对它进行分配担保,因此可以选择“标记-清理”或“标记-整理”算法。
3.什么时候回收
GC有三种:
- Minor GC:对年轻代(包括Eden和Survivor区域)回收内存叫做Minor GC。
- Major GC:对老年代回收内存称为Major GC。Major GC速度一般比Minor GC慢10倍以上。
- Full GC:对整个堆进行内存回收,在最近几个版本的JDK里默认包括了对永久代即方法区的回收(JDK8中无永久代了)。
Minor GC触发时机:
当Eden区满时,触发Minor GC。
Full GC触发时机:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行。
- 老年代空间不足
- 永久代(方法区)空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 当Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小