虽然java提供了自动垃圾回收管理机制,但是如果因此不注意,经常会出现OOM等异常,学习jvm 对垃圾回收的特性,能让我们在出现这种异常错误时,能去更好的排查解决问题。下面是我个人在参阅了周志明教授和其他博客后,对java垃圾回收的理解。
一,首先清楚,gc回收的地方:jvm中把内存划分为虚拟机栈,方法区,程序计数器,以及堆。堆和方法区是gc要回收的地方,而对象的创建是在堆中分配内存的,所以堆是垃圾回收的重点。
二,回收的对象:如果一个对象不可能再被任何地方使用,那就可以认为这个对象"已死“,那么具体是通过什么算法判断的呢?
1.引用计数算法:给对象加一个引用计数器,每当有一个地方引用它时,计数器加1,引用失效时,计数器减1,任何时刻计数器的值为0时,就认为该对象已死。
2.可达性算法:通过一系列成为“GC Roots“的对象作为起点,从这些节点向下搜索,搜索的路径成为引用链,当一个对象到GC Roots没有任何引用链时,认为该对象已死,可以作为GC Roots的对象包括:栈中reference对象引用的对象,方法区中静态对象,方法区中常量引用的对象。
引用计数法实现简单,效率也高,但是缺无法解决对象之间互相循环引用的问题。
如:
public class ReferenceCount(){
public Object instance = null;
public static void main(String[] args){
ReferenceCount obj1 = new ReferenceCount();
ReferenceCount obj2 = new ReferenceCount();
obj1.instance = obj2;
obj2.instance = obj2;
obj1=null;
obj2=null;
System.gc();
}
}
所以java 虚拟机采用的是可达性算法。
三, 回收:回收过程分为两次标记。第一次标记就是通过可达性算法,判定一个对象不存在引用链时,进行标记,然后进行筛选判断对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法或者jvm已经执行了一次finalize()方法,jvm视为没必要执行。
如果有必要执行finalize()方法,第二次标记成功,这个对象就会放到F-Queue 这个队列中,然后会由jvm自动建立的一个叫做finalizer的低优先级的线程去执行。这是一个非阻塞线程,只会去触发finalize()方法,而不会等待一个对象完整执行完finalize()方法。
所以,当一个对象不存在引用时,不一定会被回收,它可以在第二次标记阶段进行自我拯救,比如,在finalize()方法中给自己建立链接。
public class FinalizeGc {
public static FinalizeGc FC = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method invoke");
FC = this;
}
public static void main(String[] args) throws Exception {
FC = new FinalizeEscape();
FC = null;
System.gc();
Thread.sleep(500);
if(FC != null)
System.out.println("i am alive");
else
System.out.println("i am dead");
FC = null;
System.gc();
Thread.sleep(500);
if(FC != null)
System.out.println("i am alive");
else
System.out.println("i am dead");
}
}
finalize method invoke
i am alive
i am dead
四:回收算法
java内存中对堆的划分为新生代(1/3)和老年代(2/3),新生代又分为Eden区(8/10)和两块Survivor区(各1/10),Survivor区由From Space和To Space
区组成,新建对象默认都会在新生代的Eden区分配内存,对象过大时会在使用Eden区和一块Survivor区,因为新生代的对象,存活时间短,会采用复制算法,每次Minor GC,会把Eden区和Survivor区存活的对象复制到另一块Survivor区上,然后再清理掉Eden区和Survivor使用的内存,如果另一块Survivor区没有空间存放上一次Minor GC存活下来的对象时,这些对象就会通过分配担保机制进入老年代。
而老年代的对象存活时间长,稳定,所以采用标记-整理算法,将要清楚的对象标记,然后存活的对象向一端移动,然后清理掉边界以外的内存。
新生代对象是如何进入到老年代呢?
有四种方式:
1.大对象直接进入老年代。需要大量连续内存空间的对象,比如很长的字符串及数组。
2.对象在新生代创建后,它的初始年龄就是1,每次GC,在Survivor区熬下来,年龄就加1,默认到15岁时,就会进入老年代。
3.动态年龄判断:如果Survivor空间中相同年龄的所有对象大小总和大于Survivor区空间的一半,那么年龄大于或者等于该年龄的对象直接进入老年代。
4.分配担保机制:如果在新生代Minor GC之前,JVM会先检查老年代的连续可用空间是不是大于新生代所有对象的总空间,如果大于,那么这次MinorGC确保是安全的,如果不安全,就会查看JVM设置是否允许担保失败,如果允许,会去检查老年代的连续可用空间是不是大于历次进入老年代对象的平均大小,如果大于,那么将会冒险进行一次Minor GC,然后把Survivor区不能容纳的对象直接进入老年代。
Minor GC --从新生代空间回收内存成为Minor GC,当JVM无法为一个新对象分配空间时触发Minor GC 比如Eden 区满了。
Major GC --从老年代空间回收内存,Major GC 是有Minor GC 触发的
Full GC --清理整个堆空间-包括年轻代和老年代。最多是因为老年代内存也满了,触发Full GC。
内存回收的具体体现--垃圾收集器
之间的连线表示可以相互组合使用
Serial: 单线程,在GC时,会Stop The World,停止所有用户线程。
ParNew:Serial的多线程版。
ParallerScavenge:关注达到一个可控制的吞吐量,吞吐量=运行用户代码/运行用户代码+GC时间。
Serial Old:Serial的老年版。
Paraller Old:ParallerScavenge的老年版。
CMS:(Concurrent Mark Sweep)关注尽可能缩短垃圾收集时用户线程的停顿时间
过程:1.初始标记(会发生Stop The World)
2.并发标记(时间长,可以同用户线程一起工作)
3.重新标记(会发生Stop The World)
4.并发清楚(时间长,可以同用户线程一起工作)
优点:并发收集,低停顿
缺点:对cpu资源敏感,无法处理浮动垃圾,"标记-清除"算法会产生空间碎片。
G1:特点:1.并行与并发
2.分代收集:不需要其他收集器配合,独立管理整个GC堆
3.空间整理:不会产生内存空间碎片。
4.可预测的停顿。
过程:1.初始标记
2.并发标记
3.最终标记
4.筛选回收