[转载] java垃圾回收机制

时间:2023-12-05 11:25:50

转载自http://blog.csdn.net/randyjiawenjie/article/details/7551228

http://www.daniel-journey.com/archives/139

另外推荐三篇很棒的文章

JVM调优总结

Java 6 JVM参数选项大全

一次Java垃圾收集调优实战

GC的概念

GC是一种自动内存管理程序,与之相对应的是C++采用的内存管理方式。GC主要的职责就是分配内存;保证被引用的对象始终在内存中;把不被应用的对象从内存中释放。被引用的对象称之为Live 对象;不被引用的对象就是Dead对象,是需要回收的。任何事物都有光面和黑暗两面,原因很简单GC是一个很复杂的东西:-)。

GC会自动计算对象被引用的情况,只要对象不在被引用,相应的内存就会被回收,而C++中需要开发人员通过代码来“显示”地回收内存,如果程序员没有回收就会导致内存的泄露(内存泄露的原因有很多种这只是其中一个)。C++中还有经常出现的一个问题是一个对象在还有其他引用存在的情况下,就被程序给回收了,导致其他引用访问该对象时出现严重错误。另外,GC非常重要的一点就避免内存碎片,道理跟windows的磁盘

整理一样,把使用中各个内存块整合起来,这样才能保证有足够的空间来存储大对象。

理想的GC

一个理想的GC要能够满足以下几点:

·        该回收的回收,不改回收的绝不回收

·        GC要快而且GC运行的时候不能导致应用程序的停顿。

·        限制内存碎片,对象被回收以后,所使用的内存会被回收,如果不加处理内存中就会出现大量的内存碎片,这样就有可能导致因为没有足够的连续空间分配给某些大对象而导致OutofMemory。消除内存碎片的的手段之一就是“内存压缩”。

·        可扩展性(Scalability),内存的分配和回收都不能成为应用程序的瓶颈。

GC的设计选择

·        串行回收(Serial)VS并行回收(Parallel)

串行就是不管有多少个CPU,始终只有一个CPU用来执行回收操作,而并行就是把整个回收工作拆分成多个,由多个CPU同时执行。并行回收执行会快,但复杂度增加,另外也有其他一些副作用,比如内存碎片会增加。

·        并发执行(Concurrent)VS应用程序停止(Stop-the-world)

Stop-the-world的GC方式在执行GC的同时会导致应用程序的暂停。并发执行的GC虽然不会导致应用程序的暂停(这点描述不准确,后面的CMS收集器仍然会造成程序的暂停,虽然时间很短。后面的ITEYE的那个讨论也看得出来,没法阻止应用程序暂停,只是时间而已),但由于并发执行GC要解决和应用程序的执行冲突(应用程序可能会在GC的过称中修改对象),并发执行GC执行的消耗会高于Stop-the-world,而且执行也需要更多的内存堆。

·        压缩(Compacting)VS不压缩(Non-compacting)VS拷贝(Copying)

为了减少内存碎片,支持压缩的GC会把所有的活对象搬迁到一起,然后将之前占用的内存全部回收。不压缩式的GC顾名思义就是在GC的过程中不压缩内存,较之压缩式的GC,不压缩式的GC回收内存快了,而分配内存慢了,而且无法解决内存碎片的问题。拷贝式的GC会将活对象拷贝到不同的内存区域中,这种方式的优点是源数据可以被认为已经清空并可以用来分配,缺点也很明显,需要拷贝数据和额外的内存。

GC的性能评判标准

·        生产力(Throughput

全部时间中不用于GC的比例。

·        GC的开销

全部时间中用于GC的比例

·        暂停时间

GC过程中应用程序执行暂停的时间。

·        GC的频率

通过跟应用程序的执行比较来得到GC的执行频率。

·        支持GC运行所需使用的内存大小

例如heap的大小。

·        GC的及时性(Promptness)

一个对象从被废弃到内存被回收之间的时间差。

Hostspot虚拟机采用了“分代回收”(我注:关于分代的概念,可以去看看上篇问章)的策略,而“分”的非常重要的一个依据就是根据对象存在的时间的长短分成若干个“代(Gerneration)”,每个代上可以采取不同的GC策略。而采用这种“分代回收”策略是利用了2条潜规则,而且这两条潜规则不只限于Java

·        绝大对数的对象不会被长时间引用,这些对象在他的“青年期”就会被回收。

·        几乎不存在很老和很新对象之间的引用

依据这两条潜规则,Hotspot分离出新生代(Yound Generation)和旧生代(Old Generation)。由于新生代的空间通常都比较小而且可能存在大量不再被引用的对象,所以针对新生代的GC执行频率高、速度快。

在新生代中存在了一定时间还没被GC掉的对象最终会被提升到旧生代。旧生代空间比新生代的要大,但它的占用率增长会比较缓慢,因此,旧生代的GC执行频率低,但需要更长的时间来完成。

[转载] java垃圾回收机制

对新生代的GC侧重的是速度而且执行频繁,与此相反旧生代的GC侧重的是空间的利用率,即便在旧生代GC频率低的情况下依然要能够正常地工作。

另外还有一个永生代,永生代中的保存的对象都是JVM用来方便管理GC的,例如类和方法对象以及它们的描述对象。

新生代由一个Eden区域和2个survivor空间构成。绝大多数对象先分配到Eden中(有一些大的对象会可能会直接分配到旧生代中),survivor空间中的对象至少经历过一次新生代的GC,所以这些对象在被转移到旧生代之前都先暂且保留在survivor空间中。同一时间两个survivor空间中有一个用来保存对象,而另一个是空的,用来在下次的新生代GC中保存对象。

[转载] java垃圾回收机制

垃圾回收的种类

当新生代被占用满了,就会运行新生代的垃圾回收(也叫做次要回收)。当永生代或老生代满了以后,就会进行全回收(也叫做主要回收),也就是说所有的代都会被收集。通常对新生代会先被收集,而且使用的收集算法也是特别针对新生代的。然会都对老生代和永生代也进行收集。如果需要进行内存的压缩,每个代都独立地进行压缩。

当出现老生代内容太多,不再能容纳由新生代提升到老生代的内容的情况,所有的收集器(除了CMS收集器)都不再运行新生代的回收算法,相反的,老生代的收集算法会用在整个heap中。CMS收集器由于不回收新生代,所用是个特列。

快速分配

在很多情况下系统中有大量的连续的内存块可以用来分配对象,这种情况下使用bump-the-pointer算法来给对象分配内存是高效的。这种算法会记录前次分配对象的末尾,当有新的对象要分配的时候就会先检查剩余的空间是否内容满足对象的分配,如果可以的话,就会更新指针并且初始化对象。

在一个对线程的应用中,分配操作需要时线程安全的,如果通过全局锁的方式来保证线程安排的话,内存的分配就会成为瓶颈。所以HotSpot采用的是Thread-Local Allocation Buffers(TLABs)。每个thread都会有它自己的buffer,例如代中的一小块。每个TLAB都只有一个thread可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的lock。只有少数不平凡的情况,thread使用的TLAB已经满了需要新的TLAB时才会需要同步操作。

串行收集器

串行收集器对新生代和旧生代的回收都是串行的(只使用一个CPU),而且垃圾回收执行期间应用程序的执行会暂停。

新生代的串行回收

[转载] java垃圾回收机制

上面这张图说明了新生代串行回收的过程。Eden中的活动对象会拷贝到初始为空的Survior空间,也就是下图中的To Survior空间,有些特别巨大的对象无法放入到To Survior空间中会被直接拷贝到老生代中,而Form Survior空间中的活动对象如果想对比较新的话就也会被拷贝到To Survior空间中,而想对比较老的话就会拷贝到老生代中。如果To Survior空间已经满了,Eden和Form Survior空间中的活动对象都会被保留下来。Eden和Form Survior空间中的活动对象在被拷贝以后就都是非活动的状态(下图中红色X标识的内容)。新生代回收以后Eden和FormSurvior空间都会被清空,只有原本空的To空间保存了活动对象。执行以后的效果应该就是下面右边那张图。

[转载] java垃圾回收机制

老生代的串行回收

对老生代和永生代的回收算法是mark-sweep-compact。这个算法有3个阶段,mark(标识回收对象),sweep(清除),compact(压缩)。在mark阶段收集器会识别出哪些对象仍然是活动的,在sweep阶段就会对垃圾对象进行回收,在compact阶段收集器执行sliding compaction,把活动对象往老生代(永生代也一样)的前端启动,而在尾部保留一块连续的空间,方便在给新对象分配空间的时候执行bump-the-pointer。下面这张图说明的就是老生代在执行垃圾回收前后的状况。

[转载] java垃圾回收机制

串行回收器的启用

在命令行中加入参数-XX:+UseSerialGC

并行回收器

并行回收器充分利用计算机的CPU提高吞吐量。

对新生代的并行回收

新生代的并行回收算法和串行回收器是一样的,只是增加了并行的能力,新生代的并行回收器仍然是stop-the-world和coping收集器,但通过在多个cpu中并发运行,降低了GC的开销并提升了应用程序的吞吐量。

[转载] java垃圾回收机制

老生代的并行回收

老生代的并行回收使用的也是串行的mark-sweepcompact回收算法,特别注意的是并行回收器对老生代的回收并没有并行处理的能力,也就是说并行回收器只对新生代并行回收。

并行回收器的启用

在命令行中加入参数-XX:+UseParallelGC

并行压缩回收器(Parallel Compacting Collector)

Parallel Compacting Collector是在J2SE 5.0update 6中引入的,跟并行收集器最大的不同是对老生代的回收使用了不同的算法,并行压缩回收器最终会取代并行收集器。

新生代的并行压缩回收

并行压缩回收器对新生代的回收算法跟并行回收器是一样的。

老生代的并行压缩回收

并行压缩回收器同样还是会引起stop-the-world效应,并行主要是体现在sliding compaction上。收集器使用了3个阶段,首先每个代在逻辑上都分配成几个固定大小的region(区域)。在Marking阶段,在应用程序中可以直接引用到的活动对象被多个GC线程所划分掉,然后所有的活动对象就可以以并行地方式来实现对它们的标注。当某个Object被鉴定为活动对象的时候,就会更新这个对象所在区块的大小以及该对象位置的信息。

接下来是summary阶段,summary阶段操作的region而不是对象了。由于每次的GC的压缩都会每一个代的左边的部分区域活动对象密度特别高,保存了多数活动对象。对这样的高密度活动对象的区域进行压缩往往不划算。所以在summary阶段会从最左边的区域开始检验每个区域的密度,当进行到某个区域中能回收的空间达到了某个数值的时候,那么收集器会判定该区域以及该区域右边的那些区域都是值得进行回收的。该区域左边的区域都会被标识成密集,不会有对象移动到这些密集区域去,而该区域和右边的区域之后都会被进行压缩,回收空间的操作。在summary阶段会计算和保存每个活动对象在每个压缩区域的第一个字节的新位置。summary阶段目前还是串行操作,虽然并行是可以实现的,但重要性不如对marking和压缩阶段的并行来的重要。

最后的压缩阶段,回收器利用summary阶段生成的数据识别出有哪些区域是需要装填的,每个GC线程就可以独立的将数据拷贝到这些区域中。这个过程就会heap在一端很密集而另一端存在大块的空闲块。

并行压缩回收器适合运行在多个CPU的机器上,而且较之并行回收器增强了对老生代的并行回收,减少了系统停顿的时间。

并行压缩回收器的启用

在命令行中加入参数-XX:+UseParallelOldGC

还可以限制并行回收线程的数量–XX:ParallelGCThreads=n

并发Mark-Sweep(标识-清理)收集器(CMS)

很多应用中更注重快速的相应时间而不是end-to-end的处理能力。对新生代的回收通常不会造成长时间的应用程序中断,而老生代特别当Heap比较大的时候会导致长时间的中断。Hotspot引入CMS的目的就是为了解决这个问题。

CMS的新生代回收

收集的方式和并行回收一致。

CMS的老生代回收

CMS对老生代的回收多数是并发操作。垃圾收集循环开始的时候需要一个短暂的暂停,称之为初始标识(initial mark ),这个阶段会识别出那些直接被引用的活动对象。然后进入了并发标识阶段(concurrent marking phase),收集器会依据在初始标识中发现的活动对象来寻找活动对象。这个时候应用程序也同时在运行,那就无法保证所有的活动对象都会被识别、标注出来。于是应用程序会再次被暂停,在这个再标识(remark)阶段,收集器会访问在并发标识阶段中被修改过的对象并完成标志。由于再标识阶段较之初始标识更重要,所以会并发运行对个线程来提升效率。

在完成了再标识以后,所有的活动对象都已经被标注出来了。接下来就可以运行并发清理阶段。

[转载] java垃圾回收机制

另外就是CMS是不进行内存压缩的。对象回收以后就会,收集器不会移动活动对象,执行完回收的内存情况就会使下面这张图的效果。

[转载] java垃圾回收机制

而且由于空闲空间是不连续的,收集器就必须要保存一份可用空间的列表。当需要分配对象的时候,收集器就要通过这份列表找到足够容纳新对象的空间,这就内存分配算法的效果肯定要比之前介绍的几种收集器使用的bump-the-pointer算法性能差。由于老生代的分配效率差了也就影响了新生代回收过程中需要将新生代对象移到老生代的效率。

另外,CMS较之前的几种回收器需要更大的Heap,原因是在标志过程中,应用程序同时在运行,同时在分配对象,因此老生代也同时在增长。此外,虽然活动对象在标示阶段都会被识别出来,但有些在标示阶段成为垃圾的对象并不能同时被回收,只有等到下次收集的时候才能被回收。

最后,由于没有压缩,所以就容易出现内存碎片。为了解决这个问题,CMS会分析通常对象的大小来预估下一步可能的需求,然后可能会对空闲的内存块分割或合并。CMS不会等到老生代满的时候才运行内存回收,而是提早到在老生代满之前就完成内存回收工作。所以CMS会利用之前内存回收的统计数据(收集所需要的时间、老生代被沾满的时间等等)然后选择一个开始回收的时间。CMS在老生代的占用率到达某个阀值的时候也会进行回收。这个阀值可以通过

–XX:CMSInitiatingOccupancyFraction=n来定义,缺省值是68.

总之,CMS较之并发收集器以某些时候稍微增加新生代的回收时间、增加Heap的使用量、减少一些吞吐量为代价减少了老生代回收过程中的停顿时间,而且CMS会要求在应用程序运行过和收集器分享处理器资源。对那些会产生比较大老生代的应用程序而言,如果运行在多处理器上,CMS是一个不错的选择。

CMS回收器的启用

在命令行行中增加-XX:+UseConcMarkSweepGC启用CMS。

增加–XX:+CMSIncrementalMode会然CMS运行在增量模式。所谓的增量模式指的是把收集器的工作分成多个时间块,然后在两次新生代的回收期间加以运行,这种方式可以更进一步减少暂停的时间。

注:http://www.iteye.com/topic/262541

这个讨论很精彩。java的gc在实时大数据处理的时候可能会遇到严重的问题。主要是由于gc中会有stop-whole-world神一样的存在。jvm会暂停程序来进行标记。