您的位置:首页 > 编程语言 > Java开发

JAVA垃圾回收机制

2014-02-12 22:31 302 查看
1. 垃圾回收的意义

       一方面,申请的资源在不在被使用以后,必须要得到释放,才能被再次申请使用。在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,即释放没用的对象。

      另一方面,JAVA的所有对象都是分配在堆内存上的。在以前所用过的程序语言中,在堆上分配对象的代价往往十分高昂。这是由于需要有专门的算法,在堆上查找一块可用的资源用于创建对象。而JAVA的堆内存管理方式却有所不同。如果把C++的堆比作一个院子,每个对象都占用院子里的一块地盘,而新创建新对象就需要在院子里找一块空闲的地盘。而JAVA的堆,更像是一个传送带。每分配一个对象,他就往前移一格。这意味着对象存储空间的分配是非常快的。当然,这样做也有一个显而易见的问题,如果JAVA的堆完全像传送带那样工作的话,会导致频繁的内存页面调度,这会显著地影响性能,最终,在创建了足够多的对象后,内存资源将被耗尽。事实上,JAVA的垃圾回收器也同时扮演了一个内存记录碎片清除器的角色。由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,也就尽量避免了页面错误,从而实现了一种高速的,有无限空间可供分配的堆模型。
  垃圾回收的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾回收算法的不完备性,早先采用的某些垃圾回收算法就不能保证100%收集到所有的废弃内存。当然随着垃圾回收算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。

2. 垃圾收集的算法分析

 
     Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
  大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾回收首先需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。
  2.1. 引用计数法(Reference Counting Collector)
  引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
  基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。
  2.2. tracing算法(Tracing Collector)
  tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.
  2.3. compacting算法(Compacting Collector)
  为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相
4000
邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
  2.4. copying算法(Coping Collector)
  该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象区和多个空闲区,程序从对象区为对象分配空间,当对象满了,基于coping算法的垃圾回收就从根集中扫描活动对象,并将每个活动对象复制到空闲区(使得活动对象所占的内存之间没有空闲间隔),这样空闲区变成了对象区,原来的对象区变成了空闲区,程序会在新的对象区中分配内存。
  一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区域区,在对象区与空闲区域的切换过程中,程序暂停执行。
  2.5. generation算法(Generational Collector)
  stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成3个世代:年轻、年老和永久。内存的分配是发生在年轻世代中的。当一个对象存活时间足够长的时候,它就会被复制到年老世代中。对于不同的世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。基于这一点,对于各世代的垃圾回收算法就可以很有针对性。

  2.6.
adaptive算法(Adaptive Collector)
  在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

       年轻世代的内存区域被进一步划分成Eden空间、From
Survivor空间、To Survivor空间等。Eden空间是进行内存分配的地方,是一块连续的空闲内存区域。在上面进行内存分配速度非常快,因为不需要进行可用内存块的查找。From
Survivor空间和To Survivor空间两个存活区中始终有一个是空白的。在进行垃圾回收的时候,Eden空间和其中一个非空存活区From
Survivor空间中还存活的对象根据其存活时间被复制到当前空白的存活区To
Survivor空间或年老世代中。经过这一次的复制之后,之前非空的存活区To
Survivor空间中包含了当前还存活的对象,而Eden空间和另一个存活区From
Survivor空间中的内容已经不再需要了,只需要简单地把这两个区域清空即可。下一次垃圾回收的时候,这两个存活区的角色就发生了交换。一般来说,年轻世代区域较小,而且大部分对象都已经不再存活,因此在其中查找存活对象的效率较高。

  而对于年老和永久世代的内存区域,则采用的是不同的回收算法,称为“标记-清除-压缩(Mark-Sweep-Compact)”。标记的过程是找出当前还存活的对象,并进行标记;清除则遍历整个内存区域,找出其中需要进行回收的区域;而压缩则把存活对象的内存移动到整个内存区域的一端,使得另一端是一块连续的空闲区域,方便进行内存分配和复制。

     JDK 5中提供了4种不同的垃圾回收机制。最常用的是串行回收方式,即使用单个CPU回收年轻和年老世代的内存。在回收的过程中,应用程序被暂时中止。回收方式使用的是上面提到的最基本的分代回收。串行回收方式适合于一般的单CPU桌面平台。如果是多CPU的平台,则适合的是并行回收方式。这种方式在对年轻世代进行回收的时候,会使用多个CPU来并行处理,可以提升回收的性能。并发标记-清除回收方式适合于对应用的响应时间要求比较 高的情况,即需要减少垃圾回收所带来的应用暂时中止的时间。这种做法的优点在于可以在应用运行的同时标记存活对象与回收垃圾,而只需要暂时中止应用比较短的时间。

  通过JDK中提供的JConsole可以很容易的查看当前应用的内存使用情况。在JVM启动的时候添加参数 -verbose:gc 可以查看垃圾回收器的运行结果。
3. 触发垃圾回收条件

      GC分为主GC和次GC。JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大。更值得关注的是主GC的触发条件,因为它对系统影响很明显。总的来说,有两个条件会触发主GC:

  (1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。

  (2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。


      为什么执行一次GC以后内存不够还要再执行两次GC呢?

  根据一篇博客:JVM概念之Java对象的大小与引用类型,在文中提到:“软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。”我认我为之所以还需要再执行两次GC以后JVM才决定是否报“out
of memory”的错误是因为这两次GC都是去回收软引用去了,如果在回收了软引用以后内存够了,那么就分配内存空间,如果回收了软引用还是内存不够,那么就报“out of memory”的错误。这也是为什么文中强调:“虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。”
4. 减少GC开销的措施


根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不
9dad
针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:


(1)不要显式调用System.gc()

  此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。这里特别需要说明的是,在代码中显示的调用System.gc(),并不一定能够进行GC,这个我们可以通过finalize()方法进行验证,即主动调用System.gc(),并不一定每次都调用finalize()方法,具体可以参考博客final
finally finalize 的区别中的代码实例。finalize()方法的特征是在对象被回收之前, 首先调用finalize()方法。


(2)尽量减少临时对象的使用

  临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。


(3)对象不用时最好显式置为null

  一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。


(4)尽量使用StringBuffer,而不用String来累加字符串

  由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer 是可变长的,它在原有基础上进行扩增,不会产生中间对象。


(5)尽量少用静态对象变量

  静态变量属于全局变量,不会被GC回收,它们会一直占用内存。


(6)分散对象创建或删除的时间

  集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片, 从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC 的机会。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: