java-Java的垃圾收集(Garbage Collection) (摘抄自网络)

Java的垃圾收集(Garbage Collection)

作者: Mac Wang
Tuesday, April 15 2003 11:46 AM

我记得在我刚开始学习Java的时候,总是有很多学习Java的前辈们在反复地为我讲述Java的优点,比如“Write Once, Run Anywhere”之类,但是我记得印象最深的还是垃圾收集器,因为可以不用自己操心内存的管理。不过当时只知道这些脏活累活都让垃圾收集器这个家伙给干掉了,其他的就不知云里雾里。而且当时还老是觉得把这么重要的事情交给这个家伙总有点不放心,毕竟没有自己亲自动手来得踏实。

不过,随着了解的深入,越来越感觉到这个家伙的可爱。毕竟我们不用再亲自去过问烦人的内存管理,不用时刻提心吊胆是不是忘记了释放哪个对象,而且这个家伙确实把内存打理得井井有条。既然这个家伙做着这么重要的事情,而且给我们带来了这么多的好处,我想我们还是有必要来了解一下它。至少,要是有一天哪个不负责任的程序员把自己程序的错误都推到垃圾收集器的身上,你就可以义正言辞地揭穿他的谎言。而且只有了解它,你才可能真正地掌握它,用好它甚至优化它。

什么是垃圾收集呢?

垃圾收集(Garbage Collection,简称GC)其实是一种动态存储管理技术。主要是按照特定的垃圾收集算法(Garbage Collection algorithm   简称GC算法)来实现自动资源回收的功能。简单地说,就是将那些程序中释放内存的代码由系统在后台自动地完成,这样的一种机制就被称作垃圾收集,能提供这种功能的编程语言,我们就说它支持GC,比如典型的Java、C#、Python等,本文主要讨论HotSpot(详见说明一)系列Java虚拟机(JVM)中的GC。

为什么采用GC?

我相信每个初次接触GC的人都会有此一问?想要弄清楚GC如此流行的原因,就必须首先了解它的作用,它到底能干些什么?

显而易见它省去了程序员自己管理内存的麻烦和危险。在没有GC的年代,我们必须及时地释放我们霸占的内存,但同时在释放之前又必须弄清楚这些内存是不是能够被释放。由于人工管理内存的不确定性,很容易造成内存泄漏(memory leak)或者悬空指针(dangling pointer)甚至系统崩溃,这对于许多要求高性能的应用来说无疑是不可接受的。而且,系统内存是这么的娇贵,任何的不小心都可能是致命的,因此GC的使用也一定程度上提升了系统的安全。

同时,我觉得GC的流行也是当今计算思想的一种趋势。现在的操作系统和编程工具越来倾向于提供足够多的缺省服务,程序员只需要了解如何来使用这些服务就可以。乐观主义者当然认为这有利于我们从繁重的低级工作中解脱出来,有利于我们将有限的时间投入到更重要的事情之中。而悲观论者则认为,虽然麻烦和危险,但我们还是需要尽可能地自己来管理内存。而且随着越来越多的底层机制的缺省实现,程序员变得越来越傻瓜,这和最初的使用计算机的思想也是格格不入的。本来我们只是需要了解计算机是如何工作,是如何实现该某某功能,现在却演变成某某厂家的某某产品是如何使用的,这也许就是现代化的悲哀吧!

此外,GC本身的缺陷也在一定程度上影响着GC的使用,比如性能问题以及GC算法的不完备性,早先采用的某些GC算法就不能保证100%收集到所有的废弃内存。当然随着GC算法的不断改进以及软硬件运行效率的不断提升,这些问题似乎都可以迎刃而解。


GC是如何工作的?

我们前面说过,GC的工作方式是由具体的GC算法来决定的。而采用了某种GC算法的收集器(Collector)我们称之为某某垃圾收集器(Garbage Collector)。目前Java中采用的垃圾收集器(实际上每种垃圾收集器至少对应着一种GC算法)一般包括:标记/清扫收集器(Mark and Sweep Collector)、标记/缩并收集器(Mark and Compact Collector)、结点复制收集器(Copying Collector)、增量收集器(Incremental Collector)、分代收集器(Generational collector)以及高级并发收集器(见参考资料一)。当然这几种垃圾收集器往往都是交叉使用的,比如在HotSpot系列Java虚拟机中缺省的配置方式是采用分代收集器,针对老的分代(old generation)采取非复制标记/缩并收集算法(non-copying mark-compact collector),而对于新的分代(young generation)则采取单线程复制收集算法(single-threaded copying collector)。下面我们就以此为例来详细讲解一下GC的工作方式。

我们知道传统的GC工作方式是这样的:GC通过扫描JVM里运行的所有线程的堆栈和寄存器来执行。如果它发现指向Java堆(heap)内的指针(也就是所谓的根指针——Static pointer),GC将会继续查下去。如果该引用确实是对对象的引用,那么GC将跟随该对象内的所有引用。被引用对象以及该对象所引用的所有对象都将被标记,这样就完成了标记的过程。接下来的操作各种不同的收集器有不同的处理,标记/清扫收集器是将那些未被标记的对象的内存给回收,标记/缩并收集器回收内存后还需要将活着的对象搬移至一起,连成大的内存块,而结点复制收集器却是将所有被标记的对象整体搬迁至Java堆中的另一个区域,而将原区域内的内存全部回收。(见参考资料二)

从上我们可以看出,传统的GC每次必须扫描整个的Java堆,这样势必造成过长的停顿,这对于那些实时性强的应用来说可不是个好消息。所以分代收集器就应运而生,它主要是利用了这样一个事实:大部分的对象(95%以上)的生存周期都非常短,只有相当一小部分对象长期驻留在内存。通过将新创建的对象隔离到单独的区域(新的分代),可以带来至少两点好处:

  • 由于新创建的对象得以在该区域使用类似堆栈(statck-like)的分配方式,因此对象分配变得非常快速;
  • 当每次发生内存溢出需要进行GC操作的时候,该区域内的大部分对象都已经被废弃,所以仅需处理极少量的活着对象,用结点复制的收集方法只需要拷贝少量对象,从而避免对大量废弃对象的回收工作,所以该区域内经常采用复制收集算法来完成GC操作;

而对于老的对象所处的区域(老的分代),由于这些对象存活期很长,极少被销毁。所以活着的对象很多,而且排列很整齐,所以不宜采用新的分代使用的节点复制方法,而可以采用效率更高的标记/缩并收集算法。

此外,为了更好地利用系统的CPU资源(比如多个处理器的情况下),在1.4.1版本的HotSpot JVM中针对新的分代还允许采用可选的多线程结点复制收集器(multithreaded copying collector 也称为parallel collector),多个线程并行地完成追踪(tracing)和拷贝(copying)对象(live objects)的任务,极大地缩短了系统等待的时间。你可以使用-XX:+UseParNewGC参数来开启该收集器(见参考资料三)。

图1:新分代的并行收集器

 

这个并行的收集器采用深度优先的原则(depth first order)来将相关对象复制到一起,这样可以提高内存的区域性(locality)和缓存的利用(cache utilization)。在以后的JVM版本中,老的分代的收集器也会考虑使用并行收集器。

在一般情况下,JVM只在新对象所在区域内进行GC操作,只有当系统发出请求或者内存不够用的情况下,老的分代的垃圾收集才开始工作。

虽然采取了以上这些优化措施,但是还是存在用户等待。特别当系统使用大内存的时候,由于存在的对象非常多,GC的时间也就成比例上升。而同时在许多的交互式系统里是不允许存在较长的停顿期的,为了解决这个矛盾,在HotSpot系列JVM中还提供了另外一种可选的垃圾收集器——增量垃圾收集器(incremental Low-Pause Garbage Collector),它采用化整为零的策略利用Train算法(见参考资料四中的介绍)来将一次大的停顿转化为多次的小的停顿。你可以利用参数-Xincgc来开启该收集器。

图2:增量垃圾收集器

此外,为了解决老的对象存储区域GC操作时的大的停顿,在1.4.1版本的HotSpot JVM中还实现了一种可选的近乎并发的标记/清扫收集器(Mostly Concurrent non-copying Mark-Sweep Collector  简称CMS Collector),它将整个的标记/清扫工作分成了四个阶段:

  1. 标记初始阶段(Initial mark):在这个阶段系统标记根指针直接引用的对象
  2. 并发标记阶段(Concurrent marking):在这个阶段完成大部分对象的标记工作
  3. 标记完成阶段(Remark):在这个阶段,完成标记的收尾工作
  4. 并发清扫阶段(Concurrent sweeping):将所有未被标记的对象清扫掉

其中Initial mark和Remark两个耗时非常少(1G大小的老的分代,大概只需耗时200ms甚至更少)的阶段需要阻塞应用线程来进行处理,而完成大部分工作的Concurrent marking和Concurrent sweeping两个阶段则利用空闲的CPU资源来处理,这样就使得应用系统的停顿期缩短到最小。虽然在系统高峰期可能会造成一定量的性能下降(因为大家都在干活,就不存在什么空闲的CPU资源),但是停顿的平均时间和最长时间都会有一两个数量级的下降。你可以利用参数-XX:+UseConcMarkSweepGC来开启该收集器。

图3:并发的标记/清扫垃圾收集器

当然,你可以同时在新老分代里使用这些可选项来达到满足自己需求的性能。下图就是结合使用并发收集器(新的分代)和CMS收集器(老的分代)的一个演示。

图4:同时使用Parallel Collector和CMS Collector的情况

GC的特点

从上面的介绍中我们可以看出,现代GC主要具备以下几个特点:

1、  GC的精确性(Accuracy)。这主要包括两个方面:一是垃圾收集器能够精确标记活着的对象,二是垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。正是由于Java HotSpot GC能够提供百分百的精确,所以它能够保证以下两点设计的正确实现:

  • 所有不可达对象(inaccessible object)都能够可靠地得到回收;
  • 所有对象都能够重新分配(relocated),允许对象的复制和对象内存的缩并(object memory compaction),这样就有效地防止内存的支离破碎(object memory fragmentation)。

而传统的保守GC就不能做到这些。

2、  GC发生的不可预知性。最原始的GC总是发生在系统内存分配发生错误时,而现在的GC由于实现了不同的GC算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的GC一样,等到内存消耗出现极限时发生(当然不一定是内存完全消耗完),这和你的垃圾收集器的选择和具体的设置都有关系。

3、  大部分时候,GC是个同步操作,也就是在进行GC的过程中,不能同时进行任何其他的处理,不过现代GC也大量应用了多线程和异步方式来缩短单次的系统等待时间和利用空闲的系统资源。这使得采用了现代GC的应用系统具有更好的可扩展性,可以满足更高要求的应用需求。

4、  GC的实现和具体的JVM(Java Virtual Machine)以及JVM的内存模型(Memory Model)有非常紧密的关系。不同的JVM可能采用不同的GC,而JVM的内存模型决定着该JVM可以采用哪些类型GC。现在HotSpot系列JVM中的内存系统都采用最先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的GC。

5、  现代GC在清扫阶段都实现了提高内存区域性的算法。

6、  现代GC都提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这给我们根据不同的应用环境获得最优的应用性能提供了方便,但同时也给我们带来了配置的复杂性和难度,尤其是对于那些初学者来说。正所谓,利剑可以置敌人于死地,也可能会伤害自己。

随着各种新技术的应用,现代GC变得越来越庞大和复杂。所以只有真正了解和掌握了现代GC的特点,才可能充分发挥它的优点,避免它的不足,达到最优的使用效果。

JAVA编程与GC

文章写到这,我想大家应该对GC有个大概的了解,下面我还想向大家提醒几个容易犯迷糊的地方:

1、不要试图去假定GC发生的时间,这一切都是未知的。从上面的讨论中我们可以看出,GC可能发生在任何时候。比如方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放,注意这个时候仅仅是可以被释放,但是具体什么时候被释放,谁也不知道。

2、Java中提供了一些和GC打交道的类,而且提供了一种强行执行GC的方法——调用System.gc(),但这同样是个不确定的方法。Java中并不保证每次调用该方法就一定能够启动GC,它只不过会向JVM发出这样一个申请,到底是否真正执行GC,一切都是个未知数。

3、挑选适合自己的垃圾收集器。从上面我们看到,在HotSpot系列的JVM中实现了许多可选的垃圾收集器,其中许多采用了非常先进的算法。那到底那个才是真正适合我的呢?是不是越先进的越好呢?答案当然是否定的,因为先进是相对的,它可能是以牺牲其他方面作为代价的,况且最适合自己的才是最好的。一般来说,如果您的系统没有特殊和苛刻的性能要求,你可以采用JVM的缺省选项。否则你可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。系统具有较高的配置,有比较多的闲置资源,可以考虑使用CMS收集器。当然选择合适的收集器是一个方面,而合理的GC配置也是你应该注意的另一面,而且这往往很容易被忽视。

4、最关键也是最难把握的问题,就是内存泄漏。我需要单独把它列出来。

内存泄漏

提到GC,就不得不提起内存泄漏。正所谓“乱世出英雄”,当初正是由于内存泄漏这些妖魔作怪,人们才发明了GC这个利器,而且经过各位大侠的点拨指点,GC已经功力大增,在江湖上已经能够独挡一面,而且也渐渐得到各位江湖人士的认可。但是千万不要麻痹大意,以为有了GC就可以高枕无忧,那就大错特错!为了避免内存泄漏的重新作恶,你必须时刻查看以下几个锦囊:

1、良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大黑洞;

2、特别注意集合对象,比如hashtable和vector等;

3、当对象间关联引用时,也可能就是内存泄漏的滋生之处。

关于更多这方面的讨论,可以阅读参考资料5中的文章,其中详细讨论了内存泄漏产生的原因以及如何预防泄漏的发生,而且还介绍了一些检查内存泄漏的工具。

说明

1、  Java HotSpot 性能引擎(Java HotSpot performance engine)在1999年4月27日正式发布,它主要通过几项关键技术的使用来达到完美的性能提升,本文中称采用了该技术的JVM为HotSpot系列JVM,详细介绍可以参考Steve Meloan在Java.sun.com中的文章The Java HotSpotPerformance Engine: An In-Depth Look。

2、  文中部分图片(图1、图3和图4)来自参考资料3中“Turbo-charging the Java HotSpot Virtual Machine, v1.4.x to Improve the Performance and Scalability of Application Servers”一文,其中绿色箭头代表运行在多CPU上的多线程应用,红色箭头代表GC线程,其长度大致反映GC操作导致系统等待的时间。

参考资料

1、Nagendra Nagarajayya 和 J. Steven Mayer在java.sun.com中的文章“Improving Java[tm] Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory”。

2、关于这些收集器的详细工作过程可以参考University of Bristol的在线教学资料

3、  关于这些参数和JVM选项的更详细说明,你可以参考Alka Gupta 和Michael Doyle在developer.java.sun.com上的文章“Turbo-charging the Java HotSpot Virtual Machine, v1.4.x to Improve the Performance and Scalability of Application Servers”以及HotSpot JVM的说明文档

4、  Bill Venners Chapter 9 of "Inside the Java 2 Virtual Machine"。

5、  IBM dW Java专区中Jim Patrick的文章“处理 Java 程序中的内存漏洞”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值