垃圾收集器(Garbae Collection,GC),从诞生起就被要求完成3件事件:
1. 哪些内存需要回收?
2. 什么时候回收?
3. 如何回收?
在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。
1 . 引用计数算法
给对象添加一个引用计数器,每当被一个地方引用时,计数器+1,当引用失效时,计数顺-1,当计数器为0时,表示对象不被引用。
客观来讲,引用计数算法简单高效,但因为它很难解决对象宰相互循环引用的问题,导致无法被主流Java虚拟机所使用。
2 . 可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连时,则证明对象 不可用的。
可作为GC Roots的对象有以下几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
3 .对象引用
对象引用分为以下四类:
1.强引用,就是程序代码之中普遍存在的,类似”Object obj = new Object()”这类的引用。
2.软引用,用来描述一些还有用但并非必需的对象,对软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。可使用SoftReference来实现软引用
3.弱引用,用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。可使用WeakReference类来实现弱引用。
4.虚引用,也称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存无关紧要构成影响,也无法通过虚来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收地收到一个系统通知。可用PhantomReference来实现虚引用。
4 .对象的自救
在可达性分析算法中不可达的对象,也并非是”非死不可的“,要真正宣告一个对象死亡,至少要经历两次标记过程。如果对象 可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是该对象是否覆盖了finalize()方法,若对象覆盖了该方法并且尚未被调用过,那么该对象会被放置在一个叫F-Queue的队列之中,并在稍后由一个虚拟机自建的、低优先级的Finalizer线程去执行它。如果对象要拯救自己——只要重新与引用链上的任何对象建立关联即可。如果对象此时还未建立关联,那么基本就被回收 。
5 .回收方法区(永久代)
永久代的垃圾收集 要回收两部分内容:废弃常量和无用的类。
1.废弃对象,与回收java堆中的对象类似,但常量不再被有效引用时,将会被回收。
2.无用的类,通过满足三个条件来确定一个类是否没有用了。
2.1该在所有的实例都已经被回收了;
2.2加载该在的ClassLoader已经被回收了;
2.3该在对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
1.标记——清理算法
最基础的收集算法“标记——清理”算法分为“标记”和“清理”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它有两个不足之处:
1.效率问题,标记和清除两个过程效率都不高;
2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致分配大对象时,无法找到足够连续空间而触发一次GC。
2.复制算法
为了解决效率问题,一种称为“复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象 复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配进也就不用考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是种算法的代价是将内存缩小为了原来的一半。
现在商业虚拟机都采用这种收集算法来回收新生代,IBM公司研究表明,新生代中对象98%是“朝生夕死”,所以并不需要1:1的比例为划分空间,而是将内存分为一块较大的Eden空间和两个较小的Survivor空间,每次使用其中一块Survivor。回收地,将存活的对象复制到另一块Survivor空间中。HotSpot默认Eden和Suvivor大小比例为8:1。另外这种方式采用了使用老年代内存“分配担保”机制,当10%的Survivor空间不足以存放存活对象时,从老年代分配空间。
它的优缺点:
1.实现简单,运行高效;
2.浪费最多50%空间,否则需要额外空间进行分配担保;
3.因为需要额外空间时行担保,以应对100%存活的情况,老年代一般不能直接选用这种算法。
3.标记——整理算法
标记整理算法标记过程与“标记清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,从而不会产生空间碎片。
3.分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活的周期不同将内存划分为几块,一般是把JAVA堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中每次回收都大批对象死去,所以选用复制算法。而老年代中因为对象存活率高,没有额外空间对它进行担保,就必须使用“标记——清理”或“标记——整理”来进行回收。
1.枚举根节点
即进行可达性分析:可作为GC Roots的节点主要在全局性的引用(常量或类静态属性)与执行上下文(栈帧中的本地变量表)中。该操作会暂停用户线程(Stop The World)。在HotSpot中为了避免逐个检查类引用,使用了组称为OopMap的数据结构,在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,这样GC在扫描时就可以直接得知这些信息。
2.安全点
在OopMap的协助下可以很快完成GC Roots枚举,但导致引用关系变化的情况很多,但不可能为每条指令生成OopMap,那将会增加额外空间。为解决该问题,HotSpot才有了“安全点”的概念,即:只在“特定的位置”才会将信息记录以OopMap中,这个特定位置被称为“安全点”。当GC 发生需要暂停线程时,并非马上暂停,而是得等线程运行到“安全点”时才会暂停。
1.抢先式中断,在GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不是安全点,就恢复线程,让它跑到安全点。
2.主动式,当GC时,设置一个标记,各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
3.安全区域
安全区域可以理解为安全点的放大,指在某代码片段之中,引用关系不会发生变化。在这个区域任意地方GC都是安全的。