垃圾收集器是Java虚拟机(JVM)的核心组成部分之一,对Java虚拟机的性能有非常重要的影响。本文将介绍GC的工作原理以及对象回收算法,重点介绍JVM的分段回收技术;剖析JVM自带的GC性能分析工具;阐述如何通过命令行参数调节GC的运行,提高 GC的效率。
目录
1 前 言
2 垃圾收集器(GC)
3 GC的性能分析
3.1 衡量 GC的主要性能指标
4 调整 GC的运行参数,提高GC性能
4.1 增大堆空间
4.2 调节Young分段和Old分段的比例
5 结束语
1 前 言
自Java平台,特别是Java2平台发布以来,已经有许多软件,特别是大型服务器软件采用Java平台进行开发和部署,比如Bea公司的 WebLogic、IBM公司的 WebSphere等。
Java的成功一方面是由于Java的跨平台性、开放性和完全面向对象特征,顺应了软件的发展方向,获得了软件厂商的广泛支持;另一方面,对于广大的开发人员来说,Java平台屏蔽了内存分配和回收的复杂性,把开发人员从繁重的内存管理中解脱出来,使开发人员专注于应用本身的逻辑处理,提高了程序开发的效率。
尽管Java平台的优点很多,应用也非常广泛,但是,不乏人们对Java应用的抱怨,这些抱怨主要来自两个方面,一是Java应用占用的内存太大,二是Java应用的性能低下。引起上述两个问题的原因可能是多方面,但是通常与JVM的垃圾收集器(GarbageCollector,GC)有密切的关系。本文接下去首先研究Sun公司的JVM(JavaHotspotVirtualMachine)的垃圾收集器,分析垃圾收集的原理和算法;然后阐述如何通过JVM的命令行参数调节GC的性能。
本文以J2SE1.4.1提供的JVM为基础介绍 GC,但是不介绍J2SE1.4.1新引入的两种 GC———并行收集器(Parellelcollector)和并发紧缩—标记收集器(Concurrentmark-sweepcollector),这两种收集器都是针对多个CPU和大尺寸堆(通常超过1G)的。
2 垃圾收集器(GC)
在Java中,程序员需要通过关键字new为每个对象申请内存空间,所有的对象都在堆(Heap)中分配空间,对象的回收是由GC决定和执行的。换句话说,内存的分配是由程序完成的,而内存的回收是有GC完成的。那么 GC如何判断一个对象是否需要回收呢?
在运行的应用程序中,一个对象如果没有其他任何对象指向,那么这个对象就是垃圾对象,GC就可以回收这个对象。
一个最直截了当的垃圾回收算法就是遍历所有可及的对象,所有剩下的就是垃圾对象。通常的垃圾收集算法有:标记—清除法、标记—紧缩法和拷贝法。
在前两种方法中,都需要首先标记出所有的垃圾对象,标记垃圾对象的方法一种是引用计数法(ReferenceCounting),引用计数为0的对象就是垃圾对象,这种方法对于循环引用无能为力;一种是从根对象出发(例如全局对象)遍历所有可及的对象,标记出可用的对象,剩下的就是垃圾对象。在标记出所有垃圾对象之后,标记—清除法将无用对象占用的堆空间放入空闲队伍;标记—紧缩法与标记—清除法相似,只不过需要对堆空间进行整理以消除内存碎片,堆整理非常慢。
拷贝法与前面两种方法不同,在这种方法中,整个堆被分成相等的两块。在任意时刻,只有其中的一块用于分配对象,另一块是空的。在垃圾回收时,用于分配对象的一块称为源块,空的一块称为目的块,所有从根对象出发可及的对象都从源块拷贝到目的块,在下一次垃圾回收时,源块和目的块互换。这种方法效率很高,而且不存在内存碎片,缺点是被分配的内存只有一半是可用的,一半是空闲的。
Sun公司提供的Java虚拟机采用的是另外一种方法,称为“分段”垃圾收集(GenerationalGarbageCollection)。这种方法是基于一个事实,绝大多数对象“存活”的时间很短。研究表明,在大多数程序中,绝大多数对象(超过95%)都是临时对象,存活的时间很短。
为了充分利用这个事实,提高 GC的效率,JVM 对堆的管理是“分段”进行的,每个分段分别保存着不同年龄阶段的对象,当有一个分段被分配完时,启动相应的垃圾收集器。
如图1所示,这是Sun的JVM堆分段划分示意图:
整个地址空间被分成两段,一个“Old”分段,用于保存已经存活时间较长的对象;一个“Young”分段,存活时间还不长的对象。Young分段由一个“Eden”空间和两个“Survivor”空间组成。Young分段采用拷贝法进行垃圾回收,通常称为“辅回收(minorcollection)”,它的作用范围仅仅是Young分段中的对象,速度快,效率高,发生的频率比较高;Old分段通过标记—紧缩法进行垃圾收集,通常称为“主回收(majorcollection)”,它的作用范围是整个堆空间中的所有对象,速度慢。在一个应用程序中,频繁发生的是辅回收。
整个地址空间被分成两段,一个“Old”分段,用于保存已经存活时间较长的对象;一个“Young”分段,存活时间还不长的对象。Young分段由一个“Eden”空间和两个“Survivor”空间组成。
Young分段采用拷贝法进行垃圾回收,通常称为“辅回收(minorcollection)”,它的作用范围仅仅是Young分段中的对象,速度快,效率高,发生的频率比较高;Old分段通过标记—紧缩法进行垃圾收集,通常称为“主回收(majorcollection)”,它的作用范围是整个堆空间中的所有对象,速度慢。在一个应用程序中,频繁发生的是辅回收。
3 GC的性能分析
3.1 衡量 GC的主要性能指标
衡量 GC的性能主要有两个指标———吞吐量(Throughout)和“暂停(Pause)”。吞吐量指应用程序运行一段较长的时间后,不用在GC上的时间占总时间的百分数,包括对象分配的时间。“暂停”是由于GC运行使得应用程序暂停运行。
不同的应用程序对这两个性能指标的要求可能不一样。比如,Web应用程序,比较注重吞吐量,对于 GC引起的“暂停”可能要求不高。但是如果对于Swing界面应用,则强调响应速度,就是说每次“暂停”时间不能太长。
3.2 性能分析的方法
JVM在运行过程中记录下许多有用的信息,包括 GC的详细信息,可以通过命令行参数使JVM 输出这些信息,这些信息是我们进行性能分析的依据。目前,许多性能分析工具都是以这些 信 息 为 基 础 的,例 如,免 费 的 PerfAnal、Sun 公 司 的 HeapAnalysisTool1.0.3(HAT)等。下面介绍通过命令行参数获得JVM的运行信息的方法。
-verbose:gc
输出每次垃圾回收前、后堆的大小以及持续的时间。如下是输出片断:
[GC12818K->12728K(13772K),0.0046039secs]
[FullGC12728K->6792K(13772K),0.1376320secs]
第一条记录表示辅回放,箭头前面表示垃圾回收之前已经分配的堆大小,箭头之后表示回收之后实际的堆大小,括号中表示JVM的总的堆大小,最后一项表示运行时间。第二条记录表示主回收。主回收持续的时间比辅回收要长的多。
-Xloggc:<filename>
获得每次垃圾回收的开始时间、回收前后堆的大小以及持续的时间,输出的信息写入filename指定的文件。如下是文件片断:
5.02112:[GC5812K->2187K(65088K),0.0161182secs]
5.84255:[GC6283K->2573K(65088K),0.0173578secs]
中括号之前的数字,表示每次垃圾回收开始的时间,括号中各数据的意义与上文介绍的相同。
-XX:+PrintGCDetails
输出 Young分段和Old分段垃圾回收前后各自的大小以及每次垃圾回收持续的时间。如下是输出片断:
[GC[DefNew:592K->64k(640K),0.0040581secs]7614k->7431K
(8848K),0.0043531]secs][FullGC[Tenured:7367K->6210K(8208K),0.1380240secs]7599K-
>6210K(8848K),0.1383776secs]
第一条 记 录 表 示 一 次 辅 回 收,DefNew 所 在 的 括 号 表 示Young分段的情况,总的Young分段为640K;第二条记录是主回收,Tenured所在的括号表示 Old分段的情况,总的 Old分段为8208K。
-XX:+PrintTenuringDistribution
输出在 Young分段上,各个年龄阶段的对象所占的字节数。如下是输出片断:
Desiredsurvivorsize32768bytes,newthreshold31(max31)-age 1: 6288bytes, 6288total
Desiredsurvivorsize32768bytes,newthreshold31(max31)-age 1: 3960bytes, 3960total-age 2: 5448bytes, 9408total
年龄为age1的对象表示首次进入Survivor空间的对象,年龄为age2的对象表示首次从Survivor空间拷贝到另一个Survi-vor空间的对象,age3的对象表示在Survivor空间之前拷贝2次,age4的对象在Survivor拷贝3次,依次类推。
hreshold表示年龄大于这个数目的对象进入 Old分段,最大值为31,也就说,对象在Survivor之间最多只能拷贝30次,之后若还存活,就必须进入 Old分段。
-Xrunhprof
这个 参 数 在 冒 号 之 后 还 可 以 带 有 参 数,可 以 通 过java-Xrunhprof:help命令查看,通过这个参数可以输出相当丰富的信息,包括每个方法占用的运行时间、每个对象什么时候被分配、占用空间的大小。
4 调整 GC的运行参数,提高GC性能
4.1 增大堆空间
一般来说,在JVM 中,垃圾回收发生的频率与堆的大小成反比,因此,增加堆空间,有助于提高垃圾收集器的性能。我们可以通过命令行参数-Xms和-Xmx设置应用程序的初始堆大小和最大堆大小。为了提高内存空间的利用率,JVM在每次垃圾回收之后都要检查堆的空闲率,空闲率的范围是通过参数:-XX:MinHeapFreeRatio=<mininum> 和-XX:MaxHeap-FreeRatio=<maximun>设置的,如果空闲率低于 mininum,JVM就会增大堆,但不会超过-Xmx设置的大小;如果空闲率超过max-imun,JVM就会减小堆,但不会小于-Xms设置的大小。
以如 下 机 器 配 置 为 例:Celeron1.7G,256 兆 DDR,Win-dows2000Professional,jdk1.4.1,Tomcat4.1.12。在某次运行:
ava-Xms64M-client-jar-Duser.dir=″C:\Tomcat41″″C:\Tomcat41\bin
\bootstrap.jar″start
完全启动Tomcat需要进行21次辅回收,0次主回收;如果运行:
java-Xms2M-client-jar-Duser.dir=″C:\Tomcat41″″C:\Tomcat41\bin
\bootstrap.jar″start
则完全启动Tomcat需要进行169次辅回收,5次主回收。在 Windows环境下,JVM的缺省值如表1所示:
4.2 调节Young分段和Old分段的比例
对于一定大小的堆来说,增大Young分段,有利于减小辅回收发生的频率;反之,会使辅回收发生的频率增大。至于两种情况下,主回收发生的频率是否会有明显的变化,取决于应用程序中“长寿命”的对象数量。
命令行参数-XX:NewRatio=n,可以设置 Old分段与 Young分段之间的比例,比例越大,Old分段越大。
以上文所述及的机器配置为例,某次运行:
java-client-Xms12M-Xmx12m-XX:NewRatio=40-jar-Duser.
dir=″C:\Tomcat41″″C:\Tomcat41\bin\bootstrap.jar″start
完全启动Tomcat需要170次辅回收,0次主回收,向该服务器请求jsp页面(带有session对象),辅回收频率非常高,主回收频率也升高。如果运行:
java-client-Xms12M-Xmx12m-XX:NewRatio=2-jar-Duser.dir=″
C:\Tomcat41″″C:\Tomcat41\bin\bootstrap.jar″start
完全启动Tomcat需要21次辅回收,6次主回收,向该服务器请求jsp页面(带有session对象),主回收频率比前一种情况高得多,辅回收不再发生,甚至发生内存溢出。
产生上述情况的原因是Tomcat本身的“长寿命”对象并不多,但是,jsp运行起来后,保存客户端信息的“长寿命”对象增多,例如用户的session对象等。因此,对于“长寿命”对象比较多的应用程序,NewRatio值宜较大,增大Old分段;对于“长寿命”对象比较少的应用程序,NewRaito的值宜较小,增大Young分段。
至于Young分段的Survivor空间占 Young分段的比例,可以通过参数-XX:SurvivorRatio来设置,例如,-XX:SurvivorRatio=6设置survivor:eden为1:6,换句话说每个survivor的空间是 Young分段的八分之一。一般来说,Survivor空间不宜过大,因为,其中一个Survivor空间始终是空的。
在 Windows环境下,JVM的缺省值如表2所示。
上述实验数据针对不同配置的机器、操作系统和运行环境,可能有所不同。在应用中,需要根据实际要求和约束条件,反复试验和分析,才能得出最佳的参数配置。JVM的缺省参数,是针对小应用程序的,对于大型应用程序往往是不能够适应的。
5 结束语
虽然,JVM屏蔽了对象分配和回收的复杂性,但是并不是说开发人员可以对GC一无所知,因为GC对于应用程序的性能有着至关重要的影响,有些时候甚至成为应用程序的性能瓶颈。事实上,JVM本身提供分析 GC性能的多种途径,同时提供充足的参数用以调节GC的性能,在我们的简单实验中,这些参数对GC的性能有非常重要的影响。