1.前言
Garbage Collector, 简称GC, 虽然不是Java的首创,但确实经由Java发扬光大. 正是有了GC机制,很多的Java程序员已经不再关心程序的内存问题,也体会不到了C/C++程序员的苦与乐. 但是, 在做一些较低层的模块或是一些较大型的应用时候, GC知识还是必不可少的. 笔者曾经参与的一个大型系统, 上线后系统一直运行良好,随着业务的发展,用户数也直接上升.终于当用户数到达了200万的时候, 一个跟用户数据打交道的子系统出现了重大的问题, 几乎所有的CPU时间都被用来进行垃圾回收, 系统近乎瘫痪. 这个例子说明了内存管理和合理运用GC的重要性.
2.内存模型
要弄清楚GC机制,首先要对Java的内存模型有所了解. 下图是JDK1.4虚拟机规范的内存模型
堆和栈是JVM中最重要的两个内存区域。每一个java 线程都拥有自己的内存栈,用来存放局部变量和返回值,这同传统的C语言十分类似。栈是在线程启动的时候分配的。而所有的线程都共享一个内存堆,所有运行时的内存分配都在堆上进行,换句话说,所有的对象都是在堆上创建的。堆是在JVM启动的时候分配的,它的空间由GC控制.
Java的内存分配有三种:
从静态存储区域分配。内存在程序编译时就分配好了,比如静态变量
在栈上创建。各种原始数据类型的局部变量都是在栈上创建的,当程序退出该变量的作用范围的时候,这个变量的内存会被自动释放。
在堆中创建。对象(包括数组)都是在堆中创建的。程序在运行的时候用new关键字来创建对象,对象创建时会在堆中为其分配内存。
3. GC Performance Tuning
在我们的实践中, 常常会根据系统的实际情况,选用不同的GC collector,配合不同的参数来进行性能调整,这是一门专门的技术,称为GC Performance Tuning. Sun公司甚至有专门这样的tuning服务提供给一些客户.接下来笔者将结合自己的经验,详细谈谈如何进行tuning.
3.1 分代复制算法
从JRE1.3开始, GC都采用了分代复制算法,这个算法根据对象的生存期将对象分成两代,新创建的对象在年轻代(Young Generation),当年轻代的内存分完的时候,GC将年轻代中少数尚未死亡的对象复制到另一块年老代(Tenured Generation),然后直接更新年轻代的指针,这个动作称为次要收集(minor collection),一次次要收集的时间取决于年轻代中存活的对象的数目,当年轻代中的对象绝大部分已经死亡的话,次要收集速度很快。当年老代的内存分完的时候,GC会进行一次主要收集(major collection),因为采用了标记收集, 这个动作很慢。所以我们Tuning的策略基本上来说就是尽量减少主要收集的次数。
3.2 Tuning的指标
Tuning的目的是提高性能,但性能指标是有很多种的, 不同的情况下对不同的指标有不同的要求. 因此,在开始Tuning之前,我们要先弄清楚究竟是要提高哪些指标,而不仅仅是一个模糊的概念-“提高性能”.
吞吐量(Throughput)是指没用在GC上的时间比例,中断(Pause)是当GC时,程序没有响应的时间。这是衡量GC性能的两个主要指标,不同的应用对于这两个指标要求是不一样的,所以调优的策略也就不一样。此外还有其它指标,Footprint,影响着可伸缩性(scalability),灵敏度(Promptness),指从一个对象死亡到它占据的内存可用的时间间隔。通常来说,吞吐量跟年轻代的大小成正比,其它指标跟年轻代成反比。一般情况下,一个generation的大小不会影响到另外一个generation的收集频率和中断时间.
3.2 GC Collector
从JRE1.4开始, 虚拟机提供四种collector以供实际选用,除了Default Collector之外,还有throughput Collector, Concurrent Collector和Incremental Collector.
Throughput Collector, 顾名思义,是为了追求最大吞吐量而设计的, 可以通过参数 –XX:+UseParallelGC指定. 它对年轻代采用了并行收集的算法, 使用多个线程来进行minor collection. 实践经验是当只有一个CPU时,使用它反而会比default collector慢,因为有线程同步的开销。当有两个CPU的时候,跟Default Collector差不多。当有三个或以上CPU时,效果会比较明显。当有多个CPU且需要提高throughput的时候我们会尝试使用它.这个collector还有几个相关的参数可以进行tuning. -XX:ParallelGCThreads=<desirednumber>,用于指定线程的数量; -因为每一个线程会保留一部分tenure来进行promotion,因此在每一部分tenure的边界会产生碎片. 使用-XX:+UseAdaptiveSizePolicy可以统计垃圾收集的这些相关信息, -XX:+AggressiveHeap让JVM自动根据机器的内存和CPU数优化各种参数,通常内存会使用机器的最大物理内存。
Concurrent Collector是为了追求最小pause而设计的,通过-XX:+UseConcMarkSweepGC指定。它对年老代使用并发收集的算法,即可能地让收集跟主程序的执行并发。通常,当程序拥有long-lived的对象(意味着tenure大),且在多CPU机器上跑的时候比较适用。这个collector第一次pause的时候,终止所有application的线程,一个线程进行收集。第二次pause的时候,终止所有application的线程,多个线程进行收集。余下的pause,一个线程进行收集,并且并发执行application线程。当机器的CPU空闲较多的时候可以采用,否则它会跟application抢系统资源。同时,因为并发操作中会产生碎片,所以tenure的保留空间要更大(因为没有好的方法去得知Eden和survivor中碎片的大小, 所以要有一个连续的剩余空间大于等于Eden的空间加上非空survivor中对象的空间,否则就有可能发生major collection)。当在并发收集的过程中,如果年老代满了,那么这时JVM会停下application线程,进行一次full GC,这是一个信号提示我们去调整参数。因为收集线程和application线程并发执行,所以可能出现收集线程认为是lived的,马上就会死亡,这种垃圾称为float garbage。这样就会影响灵敏度,粗略的规则是将tenure空间提高20%来避免这种情况。(因为tenure大了,在GC之前就会有更多的对象死亡)。float garbage在下一次GC时会被收集。我们还可以使用-XX:+UseParNewGC和-XX:+CMSParallelRemarkEnabled来进一步降低pause的时间
Incremental Collector,也是为了追求最小pause,通过-Xincgc来指定. 它通过每次minor collection时都对一部分tenure进行收集来达到这个效果,但是这样的整体throughput是最低的。当年老代较大,年轻代较小且只有一个CPU的时候可以考虑使用。当使用Default Collector并调整generation的大小不能满足pause的要求的时候,我们可以考虑它。有时它也会执行一次非递增的major collection来避免out of memory。
3.3 其他GC Performance Tuning的经验
尽量用缺省的GC,除非需要用到其它GC的特性
当应用有很多线程,且主机CPU跟内存都很多的时候,先尝试-XX:+AggressiveHeap
除非是年老代不够大或者pause时间太长,否则,尽可能给年轻代大一点
无论何时,年老代都要保证剩余空间大于等于Eden的空间加上非空survivor中对象的空间,否则就有可能发生major collection
当应用有较多的reflection的时候,就要考虑永久代
我们可以通过在程序闲时显式调用GC来改善性能
有的应用在Solaris 8以上,可以通过将线程绑定到一个轻量进程上来提高性能(LWPs)
当查看GC的log时,如果minor collection收集的垃圾与整体heap的垃圾的差额等于promotion的数量,如果数量较大,说明要调大年轻代
要同时使用Throughput Collector和Concurrent Collector的话,通过-XX:+UseConcMarkSweepGC-XX:+UseParNewGC指定(注意,不是-XX:+UseParallelGC,-XX:+UseConcMarkSweepGC和-XX:+UseParallelGC不能同时使用)