本文共 3526 字,大约阅读时间需要 11 分钟。
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;当计数器为0,说明对象不在被使用,可以被回收。当计数器非0,说明对象正在被使用,不会被回收。
缺点: 无法解决循环引用。如 ObjectA.instance=B; ObjectB.instance=C; ObjectC.instance=A;这是个循环引用,把A,B,C都置空后是应该是可以被回收的,但是如果用引用计数法将不会回收他们。
以一个“GC Roots”为起点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。
如图:
在Java中,可作为GC Roots的对象包括以下几种:
1. 虚拟机栈中的应用的对象。(本地变量表中的reference类型)
2. 方法区中的类静态属性引用的对象。
3. 方法区中的常量引用的对象。
4. 本地方法栈JNI的引用的对象。
在JDK1.2之后,Java对引用进行了扩充,分为
强引用:Strong Reference 类似于Object o=new Object();
软引用:Soft Reference 无内存时回收,可用做缓存
弱引用:WeakReference 触发GC,则回收,可避免内存泄露
虚引用:PhantomReference 用处不大,多为虚拟机底层用到
提到垃圾回收,不得不提finalize方法。针对根搜索算法,垃圾回收需经历两个标记阶段:1. 如果进行根搜索后发现没有引用链,则进行第一次标记,然后进行一次筛选,判断是否需要执行finalize方法。
1.1如果对象覆盖了finalize方法,要处理一些事情,则把对象放入一个F-Queue的队列,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。
1.2如果对象没有覆盖finalize方法,或则finalize方法已经执行过了,则不进入F-Queue队列。
2. 如果F-Queue有值,则进行第二次标记,如果finalize方法中重新给对象一个引用,则对象被移除F-Queue队列。
实例:
package com.mylearn.jvm; /** * Created by IntelliJ IDEA. * User: yingkuohao * Date: 13-11-22 * Time: 上午10:20 * CopyRight:360buy * Descrption: * To change this template use File | Settings | File Templates. */ public class FinalizeTest { public static FinalizeTest finalizeTest = null; public static void main(String args[]) { finalizeTest = new FinalizeTest(); test(); //第一次test会执行finalize方法 test(); //第二次不会执行,finalize方法只执行一次 } private static void test() { finalizeTest = null; //引用释放 System.gc(); //gc try { Thread.sleep(500); //等待 finalizer执行 } catch (InterruptedException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } if (finalizeTest == null) { System.out.println("对象已死"); } else { System.out.println("对象尚存活"); } } protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize executed!"); finalizeTest = this; //重新给对象一个引用 } } |
结果:
finalize executed! 对象尚存活 对象已死 |
大概了解一下,Finalize尽量别用。
Mark-Sweep算法:分为标记和清除两个阶段:
1. 标记出所有需要回收的对象。
2. 清除掉所有标记的对象。
缺点:
1. 效率太低
2. 标记清除会产生大量的内存碎片。
为了解决效率问题,复制算法出现。它将可用内存划分为两块,每次使用一块,当这一块用完了,把存活的对象复制到另一块,然后把使用过的这一块一次性清理。
优点:没有碎片,移动栈顶指针,简单高效。适用于对象存活率很低的区域。
缺点:浪费一半内存
Mark-Compact:
如果对于对象存活率很高的对象,采用复制算法效率会很低。
1. 标记,同标记清除算法
2. 不去清理,而是让存活的对象都向一端移动,然后清理掉端边界以外的内存。
这个是大家常说的一种,其实还是用的前面的三种算法。
由于新生代的对象都是朝生夕死,且存活率非常低,98%的对象都会死。根据复制算法的特点,处理低存活率的对象非常高效。所以新生代就用了复制算法。又由于一半的空间太浪费,而98%对象都会死,所以产生了Survivor From和Survivor to空间,8:1:1的比例,能更好地利用内存。这样每次只有10%的内存被浪费,而不是50%。
年老代的对象存活率很高,不适用复制算法,一般采用标记整理货标记清除算法。
火车算法主要是把内存分块,每次处理一块,处理完后再处理下一块,渐进式收集。主要是为了消除用户可察觉到的垃圾回收停顿。相当于是G1收集器的一个基础。
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。简单介绍一下:
串行收集器,收集的时候停止所有其他工作线程。Stop the World! 不适合做server端。常用作Client模式下的默认新生代收集器,简单而高效。复制算法+串行
并行新生代收集器,跟Serial比起来不过多起了几个GC线程,并行而已。其他一样,但它却是许多运行在Server模式下的虚拟机首选的新生代收集器。复制算法+并行。
通ParNew类似,也是复制算法,并行的收集器。但它的关注点不同,CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标则是达到一个可控制的吞吐量。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。复制算法+并行+吞吐量优先。
是serial收集器的年来代版本,采用标记-整理算法。
ParNew收集器的年老代版本,标记-整理算法+多线程。
Concurrent Mark Sweep:以获取最短回收停顿时间为目标的收集器。目前很大一部分的应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统的停顿时间最短,以给用户带来较好的体验。用的标记-清除算法。具体包括四个阶段:
1. 初始标记
2. 并发标记
3. 重新标记
4. 并发清除
优点:并发收集,低停顿
缺点:
1. 对CPU资源敏感
2. 无法处理浮动垃圾
3. 产生空间碎片。
一个概念模型。
1. 基于“标记-整理”算法,不会产生碎片
2. 可以非常精准地控制停顿。
G1将整个Java堆划分为很多大小固定的独立区域,并跟踪他们的额垃圾堆积程度,在后台维护一个列表,每次回收垃圾最多的区域,所以叫做Garbage First。
参考:《深入理解Java虚拟机》,《深入Java虚拟机》