【深刻Java虚拟机】之八:Java垃圾收集机制

对象引用

    Java中的垃圾回收通常是在Java堆中进行,由于堆中几乎存放了Java中全部的对象实例。谈到Java堆中的垃圾回收,天然要谈到引用。在JDK1.2以前,Java中的引用定义很很纯粹:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。但在JDK1.2以后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。java

  • 强引用:如“Object obj = new Object()”,这类引用是Java程序中最广泛的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:它用来描述一些可能还有用,但并不是必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2以后提供了SoftReference类来实现软引用。
  • 弱引用:它也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存岛下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2以后,提供了WeakReference类来实现弱引用。
  • 虚引用:最弱的一种引用关系,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的是但愿能在这个对象被收集器回收时收到一个系统通知。JDK1.2以后提供了PhantomReference类来实现虚引用。

 

垃圾对象的断定

    Java堆中存放着几乎全部的对象实例,垃圾收集器对堆中的对象进行回收前,要先肯定这些对象是否还有用,断定对象是否为垃圾对象有以下算法:算法

    引用计数算法

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

    引用计数算法的实现简单,断定效率也很高,在大部分状况下它都是一个不错的选择,当Java语言并无选择这种算法来进行垃圾回收,主要缘由是它很难解决对象之间的相互循环引用问题。数组

    根搜索算法

    Java和C#中都是采用根搜索算法来断定对象是否存活的。这种算法的基本思路是经过一系列名为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证实此对象是不可用的。在Java语言里,可做为GC Roots的兑现包括下面几种:服务器

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

    实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:若是对象在进行根搜索后发现没有与GC Roots相链接的引用链,那它会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为没有必要执行。若是该对象被断定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动创建的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(由于一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是要在finalize()方法中成功拯救本身,只要在finalize()方法中让该对象重引用链上的任何一个对象创建关联便可。而若是对象这时尚未关联到任何链上的引用,那它就会被回收掉。网络

 

垃圾收集算法

    断定除了垃圾对象以后,即可以进行垃圾回收了。下面介绍一些垃圾收集算法,因为垃圾收集算法的实现涉及大量的程序细节,所以这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。ide

    标记—清除算法

    标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉全部被标记的对象,它的标记过程其实就是前面的根搜索算法中断定垃圾对象的标记过程。标记—清除算法的执行状况以下图所示:性能

    回收前状态:优化

    回收后状态:

 

 

    该算法有以下缺点:spa

  • 标记和清除过程的效率都不高。
  • 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不触发另外一次垃圾收集动做。

    复制算法

    复制算法是针对标记—清除算法的缺点,在其基础上进行改进而获得的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,而后再把已使用过的内存空间一次清理掉。复制算法有以下优势:

  • 每次只对一块内存进行回收,运行高效。
  • 只需移动栈顶指针,按顺序分配内存便可,实现简单。
  • 内存回收时不用考虑内存碎片的出现。

    它的缺点是:可一次性分配的最大内存缩小了一半。

    复制算法的执行状况以下图所示:

    回收前状态:

    回收后状态:

    标记—整理算法

    复制算法比较适合于新生代,在老年代中,对象存活率比较高,若是执行较多的复制操做,效率将会变低,因此老年代通常会选用其余算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程同样,但对标记后出的垃圾对象的处理状况有所不一样,它不是直接对可回收对象进行清理,而是让全部的对象都向一端移动,而后直接清理掉端边界之外的内存。标记—整理算法的回收状况以下所示:

    回收前状态:

    回收后状态:

 

    分代收集

    当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不一样将内存划分为几块,通常是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少许存活,所以可选用复制算法来完成收集,而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

 

垃圾收集器

    垃圾收集器是内存回收算法的具体实现,Java虚拟机规范中对垃圾收集器应该如何实现并无任何规定,所以不一样厂商、不一样版本的虚拟机所提供的垃圾收集器均可能会有很大的差异。Sun  HotSpot虚拟机1.6版包含了以下收集器:SerialParNewParallel ScavengeCMSSerial OldParallel Old。这些收集器以不一样的组合形式配合工做来完成不一样分代区的垃圾收集工做。

 

垃圾回收分析   

    在用代码分析以前,咱们对内存的分配策略明确如下三点:
  • 对象优先在Eden分配。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。
    对垃圾回收策略说明如下两点:
  • 新生代GC(Minor GC):发生在新生代的垃圾收集动做,由于Java对象大多都具备朝生夕灭的特性,所以Minor GC很是频繁,通常回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,常常会伴随至少一次Minor GC。因为老年代中的对象生命周期比较长,所以Major GC并不频繁,通常都是等待老年代满了后才进行Full GC,并且其速度通常会比Minor GC慢10倍以上。另外,若是分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。

    下面咱们来看以下代码:

1 public class SlotGc{
2     public static void main(String[] args){
3         byte[] holder = new byte[32*1024*1024];
4         System.gc();
5     }
6 }
View Code
    代码很简单,就是向内存中填充了32MB的数据,而后经过虚拟机进行垃圾收集。在Javac编译后,咱们执行以下指令:java -verbose:gc SlotGc来查看垃圾收集的结果,获得以下输出信息:

    [GC 208K->134K(5056K), 0.0017306 secs]

    [Full GC 134K->134K(5056K), 0.0121194 secs]

    [Full GC 32902K->32902K(37828K), 0.0094149 sec

    注意第三行,“->”以前的数据表示垃圾回收前堆中存活对象所占用的内存大小,“->”以后的数据表示垃圾回收堆中存活对象所占用的内存大小,括号中的数据表示堆内存的总容量,0.0094149 sec 表示垃圾回收所用的时间。

    从结果中能够看出,System.gc(()运行后并无回收掉这32MB的内存,这应该是意料之中的结果,由于变量holder还处在做用域内,虚拟机天然不会回收掉holder引用的对象所占用的内存。

    咱们把代码修改以下:

1 public class SlotGc{
2     public static void main(String[] args){
3         {
4         byte[] holder = new byte[32*1024*1024];
5         }
6         System.gc();
7     }
8 }
View Code

     加入花括号后,holder的做用域被限制在了花括号以内,所以,在执行System.gc()时,holder引用已经不能再被访问,逻辑上来说,此次应该会回收掉holder引用的对象所占的内存。但查看垃圾回收状况时,输出信息以下:

    [GC 208K->134K(5056K), 0.0017100 secs]

    [Full GC 134K->134K(5056K), 0.0125887 secs]

    [Full GC 32902K->32902K(37828K), 0.0089226 secs]

    很明显,这32MB的数据并无被回收。下面咱们再作以下修改:

1 public class SlotGc{
2     public static void main(String[] args){
3         {
4         byte[] holder = new byte[32*1024*1024];
5         holder = null;
6         }
7         System.gc();
8     }
9 }
View Code

     此次获得的垃圾回收信息以下:

    [GC 208K->134K(5056K), 0.0017194 secs]

    [Full GC 134K->134K(5056K), 0.0124656 secs]

    [Full GC 32902K->134K(37828K), 0.0091637 secs]

    说明此次holder引用的对象所占的内存被回收了。咱们慢慢来分析。

    首先明确一点:holder可否被回收的根本缘由是局部变量表中的Slot是否还存有关于holder数组对象的引用。

在第一次修改中,虽然在holder做用域以外进行回收,可是在此以后,没有对局部变量表的读写操做,holder所占用的Slot尚未被其余变量所复用(回忆Java内存区域与内存溢出一文中关于Slot的讲解),因此做为GC Roots一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,所以GC收集器不会将holder引用的对象内存回收掉。 在第二次修改中,在GC收集器工做前,手动将holder设置为null值,就把holder所占用的局部变量表中的Slot清空了,所以,此次GC收集器工做时将holder以前引用的对象内存回收掉了。

    固然,咱们也能够用其余方法来将holder引用的对象内存回收掉,只要复用holder所占用的slot便可,好比在holder做用域以外执行一次读写操做。

    为对象赋null值并非控制变量回收的最好方法,以恰当的变量做用域来控制变量回收时间才是最优雅的解决办法。另外,赋null值的操做在通过虚拟机JIT编译器优化后会被消除掉,通过JIT编译后,System.gc()执行时就能够正确地回收掉内存,而无需赋null值。

 

性能调优 

    Java虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序(尤为服务器端)的性能和稳定性有着很是重要的影响。性能调优须要具体状况具体分析,并且实际分析时可能须要考虑的方面不少,这里仅就一些简单经常使用的状况做简要介绍。   

    • 咱们能够经过给Java虚拟机分配超大堆(前提是物理机的内存足够大)来提高服务器的响应速度,但分配超大堆的前提是有把握把应用程序的Full GC频率控制得足够低,由于一次Full GC的时间形成比较长时间的停顿。控制Full GC频率的关键是保证应用中绝大多数对象的生存周期不该太长,尤为不能产生批量的、生命周期长的大对象,这样才能保证老年代的稳定。
    • Direct Memory在堆内存外分配,并且两者均受限于物理机内存,且成负相关关系,所以分配超大堆时,若是用到了NIO机制分配使用了不少的Direct Memory,则有可能致使Direct Memory的OutOfMemoryError异常,这时能够经过-XX:MaxDirectMemorySize参数调整Direct Memory的大小。
    • 除了Java堆和永久代以及直接内存外,还要注意下面这些区域也会占用较多的内存,这些内存的总和会受到操做系统进程最大内存的限制:

      一、线程堆栈:可经过-Xss调整大小,内存不足时抛出StackOverflowError(纵向没法分配,即没法分配新的栈帧)或OutOfMemoryError(横向没法分配,即没法创建新的线程)。

      二、Socket缓冲区:每一个Socket链接都有ReceiveSend两个缓冲区,分别占用大约37KB25KB的内存。若是没法分配,可能会抛出IOExceptionToo many open files异常。关于Socket缓冲区的详细介绍参见个人Java网络编程系列中深刻剖析Socket的几篇文章。

      三、JNI代码:若是代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。

      四、虚拟机和GC:虚拟机和GC的代码执行也要消耗必定的内存。

        转自:http://blog.csdn.net/ns_code/article/details/18076173
相关文章
相关标签/搜索