学习JVM中的垃圾回收机制,就是要弄清楚下面三个问题
1、哪些内存需要被回收
2、什么时候回收这些内存
3、如何回收这些内存
针对以上三个问题,逐个分析
一、哪些内存需要被回收
垃圾回收器在判断一个对象是否需要被回收时,有两种算法
1、引用计数法
给每个对象添加一个引用计数器,每当有一个地方引用这个对象时,计数器加1,当引用失效时减1。当对象的引用计数器为0时,就代表这个对象可以被回收了。这种算法判断对象是否应该被回收时的效率非常高,但是无法解决对象之间的相互引用问题,目前主流的JVM都不使用这种算法。
例如A、B对象都有字段instance,A.instance = B,B.instance = A,在引用计数法中,A、B对象的计数器都是1,不会被回收,而在JVM中,很显然他们是两个孤立的对立,应该被回收。
2、对象可达性分析
这种算法可以有效的避免对象循环引用的问题,整体对象实例已一个树呈现,根节点是一个“GC ROOTS”对象,从这个对象开始向下搜索并标记,遍历完这棵树后,未被标记的对象,则是需要回收的对象。如图中Obj4、Obj5需要被回收
一下四种对象作为GC ROOTS对象使用
①虚拟机栈所引用的对象
②方法区中静态属性所引用的对象
③方法区中常量所引用的对象
④本地方法所引用的对象
二、什么时候回收这些内存
1、程序空闲的时候
2、显示调用System.gc()
3、jvm堆内存不足时
三、如何回收这些内存
以下为4中最为常见的垃圾回收算法
1、标记-清除算法
“标记-清除”算法是最基础的算法,分为标记阶段和清除阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。如图为采用此算法回收前后的内存示意图:
此算法有2个缺点,1、效率问题,标记和清除两个过程,其效率都不高;2、空间问题,图中可以清晰的看出,回收后会产生大量的内存碎片,在需要为一个大对象分配内存空间时,因为找不到合适的连续空间而不得已提前触发下一次GC。
2、复制算法
为了解决标记-清除算法的效率问题,出现的复制算法。它将可用内存按照容量分成两个大小相等的块,每次只使用其中的一块。当这一块内存用完了,就将还活着的对象复制到另一块上,然后清空这一块的内存空间统一清掉。
其优点是每次都是对其中的一块进行内存回收,不会产生内存碎片,在内存分配时,无需考虑碎片问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。其缺点也很明显,就是内存缩小了一半,代价过大。
3、标记-整理算法
复制算法在对象存活率高的情况下,每次都要复制大量对象,效率将会变低。所有老年代一般不能直接采用复制算法。针对老年代的特点提出了“标记-整理”算法,其标记过程仍然和“标记-清除”算法一样,但是后续步骤不是将可回收对象直接清除掉,而是让所有存活对象都向内存空间一端移动,然后直接清理掉边界以外的内存空间。
4、分代收集算法
当前商业虚拟机的垃圾回收都采用“分代收集”算法,该算法并不是一种新算法,而是根据对象的存活周期,将内存划分为不同的块,一般是把java堆分为新生代和老年代,然后根据它们的特点,采用不一样的算法。新生代中,每次GC都有大批对象死去,少量对象存活,因此采用复制算法,只需要付出少量对象的复制成本即可。而老年代中,对象存活率高,就必须采用“标记-清除”或者“标记-整理”算法来进行回收。