《深入理解Java虚拟机》读书笔记——垃圾回收

深入理解Java虚拟机 了解GC和内存分配,有助于我们排查各种内存溢出、内存泄露问题。当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

对象引用

堆中几乎存放着Java中所有的对象实例,判定对象是否存活都与“引用”有关。

在JDK 1.2之前,Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

在JDK 1.2之后,Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

  • 强引用即常规意义上的引用,如“ Object obj = new Object()”,只要强引用存在,对象就永远不会被垃圾收集器回收。
  • 软引用用来描述一下还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

判断对象是否存活的方法有引用计数算法、根搜索算法等。

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

计数算法(Reference Counting)实现简单,判定效率也很高,但是Java语言并没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。

根搜索算法

Java使用根搜索算法(GC Roots Tracing)判定对象是否存活。通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,则证明此对象是不可用的。

在Java语言里,可作为GC Root的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(Native方法)的引用的对象。

判断一个对象真正死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移出“即将回收”的集合;如果对象这个时候还没有逃脱,那它就真的离死不远了。注意,finalize()方法只会被调用一次。

垃圾收集算法

标记-清除算法

最基础的收集算法,后续收集算法都是基于这种思路并改进得到。

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

主要缺点:
1. 效率问题:标记和清除过程的效率都不高; 2. 空间问题:标记清除之后会产生大量不连续的内存碎片。

复制算法

为解决效率问题。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

缺点:
1. 将内存缩小为原来的一半; 2. 在对象存活率较高时要执行较多的复制操作,效率将会变低。

标记-整理算法

标记过程仍然与“标记-清除”算法一样,后续步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。

  • 新生代:每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代:因对象存活率高、没有额外空间对其进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

参考链接

http://blog.csdn.net/ns_code/article/details/18076173

发表评论

您的电子邮箱地址不会被公开。