一. 垃圾回收
垃圾回收指的是将已死对象所占内存空间释放,避免内存泄漏。java垃圾收集器是针对java堆和方法区。垃圾回收需要考虑3个问题:哪些内存需要回收?什么时候回收?如何回收?
在java堆里面存放了几乎所有对象实例,回收前需确定这些对象哪些还“活着”,哪些已经“死去”。通常有2种方法。
引用计数法:通过在对象请求头分配一个空间来保存被引用次数,每当一个地方引用它时,计数就加一;当引用失效时,计数就减一,任何时刻当计数为0时,对象就会被回收。但此方法很难解决对象间相互循环引用的问题,比如定义两个对象,并相互引用,然后置空各自声明引用,此时两个对象已经不可能被引用访问了,但由于它们互相引用着对方,计数不为0,于是引用计数算法无法通知GC收集器回收它们。
可达性分析算法:通过一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。在java语言中,可作为GC Roots 的对象有以下几种。
a.虚拟机栈(栈帧中的本地变量表)中所引用的对象。
b.方法区中类静态属性引用的对象
c.方法区中常量引用的对象
d.本地方法栈中JNI(即一般说的Native方法)引用的对象
关于引用:在JDK1.2之前,java中引用的定义:如果reference类型的数据中类型中存储的数值代表的是另外一个内存的起始地址,就称这块内存代表着一个引用。在这种定义下,对象只有被引用和没有被引用两种状态。我们希望有一类对象,当内存空间还足够时,则保留在内存中,如果内存空间进行垃圾回收后还是十分紧张,则可以抛弃这些对象。在JDK1.2之后,java对引用概念进行扩充,分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
a.强引用:类似于“Object object = new Object()”这种引用,只要强引用在,垃圾收集器永远不会回收掉被引用的对象。
b.软引用:用来描述一些还有用但并非必需的对象。对于这种对象,在将发生内存溢出之前将会把这些对象列进回收范围之中进行第二次回收。提供了SoftReference类来实现软引用。
c.弱引用:用来描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。提供了WeakReference类来实现弱引用。
d.虚引用:也称幽灵引用或幻影引用,它是最弱的引用关系。一个对象是否有虚引用完全不会影响其生存时间,也不能通过虚引用来获取对象实例。为一个对象设置虚引用关联目的只是在对象回收时收到一个系统通知。提供PhantomReference类。
关于回收:一个对象真正被回收,至少经历两次标记过程:如果对象在进行可达性分析后发现不可达,那它将会被第一次标记并且进行一次筛选,条件是此对象是否有必要执行finalize()方法。当对象没有覆盖此方法,或者此方法已被虚拟机调用过,这两种情况都视为没有必要执行。如果对象判定需要执行,那么此对象会放置在一个叫做F-Queue队列中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行,即触发此方法,但不保证会等待它运行结束,稍后GC将对队列中的对象进行第二次小规模标记,如果对象在finalize()方法中成功拯救自己(变成可达),那么第二次标记时它将被移除出“即将回收集合”,否则基本被回收了(finalize()方法只会被虚拟机调用一次,面临下次回收时无法通过finalize()自救)。
关于回收方法区:java虚拟机规范说过可以不要求虚拟机在方法区进行垃圾回收,而且在方法区回收性价比比较低。方法区主要回收两部分内容:废弃常量和无用的类。回收废弃常量跟回收java堆中的对象非常类似,如果一个字符串“abc”已经进入常量池,但是当前系统没有一个String对象引用这个常量,也没有其他地方引用,如果此时发生回收,“abc”常量就会被清出常量池,常量池中其他类,接口,方法,字段的符号引用也与此相似。判断一个常量是否废弃比较简单,但判断一个类是否无用条件比较苛刻,需满足3个条件:①该类所有实例都已经被回收②加载该类的ClassLoader已经被回收 ③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法。满足3个条件及可以进行回收,但不是必然。在大量使用反射,动态代理,CGLib等框架,动态生成JSP及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,保证方法区不会溢出。
那如何回收呢?有以下几种算法。
a.标记—清除算法。算法分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这是最基础的算法,后面的算法都是基于这种思想并改进的,它主要不足有两个:一是效率问题,标记跟清除两个过程效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,太多碎片导致后面需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
b.复制算法。为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活着的对象复制到另一块上面,然后把已使用过的内存清理掉,内存分配时也不用考虑内存碎片,只要移动指针,按顺序分配即可只是这种算法代价是将内存缩小为原来的一半,代价太大。
c.标记—整理算法。标记过程和标记—清除算法一样,但后续不是直接对可回收对象进行清除,而是让所有存活对象向一端移动,然后直接清除掉端边界以外内存。
d.分代收集法。根据对象的存活周期的不同把内存划分为几块。一般把java堆分为新生代和老年代,这样根据不同年代特点选择不同算法,在新生代中,每次垃圾回收都发现有大量对象死去,那就选择复制算法,而老年代中因为对象存活率高,就必须使用标记清理或标记整理算法进行回收。