Thursday, October 15, 2009

冒泡啦

由于有伟大的无形的墙存在,这里在墙里基本上不来,欢迎访问我的独立域名blog:


希望有朝一日不用翻墙也能到这里来,不要到了那个时候这里已经不存在了就好。

Saturday, November 15, 2008

Java SE 6 Hotspot [TM] 虚拟机垃圾回收调优



























Java SE 6 Hotspot [TM] 虚拟机垃圾回收调优




来源:http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html

王旭  [ gnawux (at) gmail.com ; http://www.wangxu.me ] 于2008年11月译
译者按:有些术语实在很别扭,不过译者文采有限,没法找到合适的中文词汇,不太影响理解,凑合看吧。




1. 概述


Java 平台标准版(Java SE™)被广泛应用于各种应用,从桌面上的小小的 applet 到大型服务器上的 Web Service 无处不在。为了支持各种不同的部署场景,Java HotSpot™ 虚拟机提供了多种垃圾回收器,每种都为满足不同的需求而设定。这是也为了满足大大小小不同应用需求的一部分。不过,那些需要高性能应用的用户、开发者和管理员们也被选择适合他们应用的恰当的垃圾回收器的繁琐困扰着。取消这些额外操作的重要一步是在 J2SE™ 5.0 中作出的:垃圾回收器会根据应用运行的计算机类型而作出选择。





这个垃圾回收器的“更好的选择”总的说是一种进步,不过,这并不意味着对所有的应用这都是最好的选择。对于有极端的性能或其他需求的用户,仍需要显式地指定垃圾回收器,并调优某些参数,以达到满意的性能。本文就为这些需求提供了一些相关信息。首先,本文会基于串行的 stop-the-world 垃圾回收器来介绍垃圾回收器的一般性特征和基本调优开关。接下来会介绍其他垃圾回收器的特点和如何选择一个垃圾回收器。




何时选择垃圾回收器?对于一些应用,这个答案可能是“永远不”。也就是说,在有低频率、短时的垃圾收集器造成的停顿的情况下,大部分程序都运行良好。不过,这并不适用于很多程序,特别是那些处理大量数据(若干GB)、很多线程和需要处理很多事务的情况。







Amdahl 观察到,大部分工作负载并不能被很好的并行化;有部分情况下总是会被顺序执行,无法从并行化中获益。这对 Java™ 平台也是如此。特别的,在 J2SE 1.4 以前,Sun Java 平台的虚拟机并不支持并行垃圾回收,这样,在多处理器系统中,垃圾回收会对并行应用产生严重影响。







下图显示了一个除了垃圾回收以外均为完美可伸缩的理想系统的性能曲线。红色曲线是一个在但处理器系统中会花费 1% 的时间在垃圾回收上的程序。它在 32 处理器的系统中,将损失 20% 的吞吐量。而一个花费 10% 时间在垃圾回收上的应用(不考虑单处理器系统中额外的垃圾回收时间)在系统扩张到 32 处理器系统中时,会损失超过 75% 的吞吐量 。



 










这意味着在小型开发系统中微不足道的速度问题当扩张到大规模系统中就可能成为严重的性能瓶颈。从另一个角度看,减少这样的性能瓶颈的小改动就可以获得很大的性能收益。对足够大规模的系统,选择合适的垃圾收集器并进行必要调优是绝对值得的。







对于大多数“小”应用(在现代处理器上大约需要100MB堆内存的应用)来说通常是足够的。其他垃圾收集器会带来额外的负载或复杂性,这回让系统的某些行为付出一定的代价。如果一个应用不需要一个垃圾收集器的某个功能。那么就使用串行的垃圾收集器好了。一个不应该使用串行垃圾收集器场景是一个超多线程的大程序运行在一个大型的、有大量内存和两个或多个处理器的系统中。当应用运行在这些服务器级的计算机上的时候,并行垃圾收集器会被缺省选择(参见下面的功效学 )。






本文以 Solaris™ 操作系统(SPARC(R) 平台版本)中的 Java SE 6 作为参考。不过,文中所述的概念和建议适用于所有支持的平台,包括 Linux, Microsoft Windows 和 Solaris 操作系统(x86 平台版本)。此外,文中的命令行参数也对所有平台有效,虽然它们的缺省值在各个平台可能有所不同。


2. 功效学(Ergonomics)







“功效学”是一个 J2SE 5.0 引入的概念。引入功效学概念是为了通过不设置或设置很少的几个命令行参数的情况下提供更好的性能,这些参数包括:




  • 垃圾收集器,



  • 堆尺寸,


  • 和运行时编译器




这里的参数选择假定应用所运行的主机类型和应用的类型一致(也就是说,大型应用运行在大型的机器上)。这些选项简化了垃圾回收的调优。选择并行垃圾回收器,用户可以指定应用的最大中断时间和希望的吞吐量。这和指定堆大小来调优性能是相对应的。最常用的功效学相关的内容在可以参考 “Ergonomics in the 5.0 Java Virtual Machine” 这篇文章。建议在尝试本文提到的细节配置之前尝试该文章中介绍的功效学手段。






本文中的功效学特性被作为并行垃圾回收器的自适应尺寸策略的一部分。这包括指定垃圾回收性能的目标和性能调优的一些附加选项。


3.代


J2SE 平台的优势之一是它将内存分配、垃圾回收这些繁复的细节屏蔽了起来。然而,一旦垃圾回收成为主要的瓶颈,那么理解一下这些隐藏在背后的细节就变得有必要了。垃圾回收器对应用程序对对象的使用方式进行判断,这个判断会反映在可调优参数中,他们可以被调整,以提高性能而不牺牲掉抽象性。




当一个对象不再可能被从其他任何地方访问到的时候就会被认为是垃圾了。最直接的垃圾回收算法就是简单地迭代所有可找到的对象。任何没有被跌带到的对象都可以被认为是垃圾了。这个方法的用时和活着的对象数量成正比,这对于那些维护着大量活数据的程序来说是不可接受的。






从 J2SE 1.2 开始,虚拟机就引入了各种不同的垃圾回收算法,这些算法都使用分代垃圾收集。尽管原生的垃圾回收会检查堆中的所有活着的对象,分代垃圾收集采用了很多观测到的大部分应用程序的经验特征,用来最小化发现废弃的对象的工作量。最重要的经验特征是 weak generational 假设,该假设认为大部分对象都只存活一少段时间。






下图中的蓝色区域是对象生存期的典型分布。横轴是对象被分配后的生存期。纵轴方向计算的字节数是相应生存期的对象的总字节数。左侧的尖峰表明,对象在分配之后不久就被废弃了。比如,迭代器对象常常只会在一个循环中被用到。








当然,有些对象确实活得要长一些,于是,分布曲线延伸到了右边。比如,典型情况下,有些对象在初始化的时候被创建,并一直存活到进程结束。在这两种极限情况之间,那些对象活的时间也是中等的,在图中表现初来的就是从开始的峰值泄漏初来的蓝色区域。有些应用可能会有看起来十分不同的分布曲线,不过绝大多数的进程都是这个常见的形状。大部分对象都会“英年早逝”这个事实让高效的垃圾收集变得具有可能性了。







为了为这样的应用环境优化,内存被按照“代” (generation)进行管理,或者说,内存池中存放不同年龄的对象。当一个年龄断被填满后,就对该代的垃圾进行回收。在内存池中的大部分对象都是年轻的对象(年轻的代),而大部分对象也会在年轻的时候就成为垃圾。当年轻代被填满的时候,会导致一次“小回收”(译注:原文minor,似乎“未成年”更贴切一些,不过咱们读起来会很别扭),这里只有年轻代的对象惠北回收,而其他年龄断的垃圾则不与理会。该回收算法的成本是,一阶情况下,正比于被回收的活的对象的数量;年轻代因为满是死对象,所以回收非常迅速。而在“小回收”中存活下来的对象于是乎就会被转移到所谓的年老代(tenured generation)。最终,当年老代被填满而需要回收的时候,就会导致一次主回收,这时整个堆都会被回收。主回收通常会运行锝比小回收慢很多,因为大量的对象都会被处理。







如上文记述,对不同的应用,“工效学”会动态选择垃圾收集器来提供较好的性能。串行垃圾收集器用于哪些数据量比较小的程序,而且它的缺省参数也让大多数小程序能够高效工作。而大吞吐量垃圾收集器用于那些有中到大数据量的数据集。工效学选择的堆尺寸参数和自适应尺寸策略用于为服务器提供更好的性能。这些选择的大多数而不是所有的情况下工作得很不错。这就引出了本文的核心宗旨:







如果垃圾收集器成为了瓶颈,你可能不得不调整整个堆的大小乃至每个代的尺寸。检查垃圾收集器的详细输出,然后检查垃圾收集器对你关注的各个性能指标的影响。







(并行垃圾收集器之外的)缺省的代排布大概就是这样的。









初始化的时候,最大的地址空间虚拟地保留住而没有分配出去,直到真的需要的时候为止。整个保留的对象地址空间被分给了年轻的和年老的代。






年轻代包括“伊甸园”和两个幸存者空间。大部分对象最初在伊甸园里被分配出来。一个幸存者空间在任意时刻都是空的,作为伊甸园中的活对象的目的地,另一个是用于下一次收集。对象在幸存者空间之间停留到足够老之后,就会被复制到年老代去了。







另一个和年老代有密切关系的代是永久的(permanent)代,这里保存着虚拟机需要的用来描述那些 Java 语言层面没有等价物的对象。比如,那些描述类和方法的的对象就存放在永久代。







3.1 性能考虑


对于垃圾回收的性能,主要有两种量度方法:





  1. 吞吐量。吞吐量是在一段足够长的时间中,没有花费在垃圾回收上的时间占总时间的百分比。吞吐量包含了花在空间费配上的时间(不过空间分配速度的调优一般是没有必要的)。



  2. 延时。延时是由于等待垃圾回收而导致的程序没有响应的时间。







不同的用户对垃圾收集有不同的需求。比如,对于一个web server而言,吞吐量是合理的量度,因为垃圾收集带来的短时时延是可以容忍的,或者说是很容易就被网络时延所掩盖了。不过,对于交互的图形界面程序而言,极短的停顿都会影响用户的使用体验。






有些用户对其他的因素很敏感。Footprint是一个进程的工作集,由页和cache line来量度。对于内存相对于进程数量很有限的系统而言。Footprint会影响到程序的可伸缩性。Promptness是对象死掉和该块内存重新可用之间的时间间隔的量度,这是分布式系统的一个重要考虑因素,包括远程方法调用(RMI)。






总的说,一个特定的代的尺寸选择是上述这些因素之间的权衡的结果。比如,一个非常大的年轻代的大小可以最大化吞吐律,但会以Footprint、Promptness和延时作为代价。而年轻代延时可以通过缩小该代的大小来达到最小化,但同样会损失吞吐量。近似地,调整一个代的尺寸不会影响到其他代的垃圾收集频率和时延。






没有一个简单的方法来设置代的尺寸。最好的选择由程序使用内存的方式和用户的需求来决定。这样,虚拟机对垃圾收集器的选择并不总是最优的,而且可以通过后面介绍的命令行参数来调整。



3.2 测量



使用应用特定的量度,吞吐量和footprint很容易被测量。例如,web服务器的吞吐量可以使用一个客户端负载生成器来测量,而该服务器的 footprint 则可以在 Solaris 操作系统中使用 pmap 命令来测量。另一方面,垃圾收集导致的时延可以方便地通过监测虚拟机自己的诊断输出来估算出来。







命令行参数 -verbos:gc 可以送出每一次垃圾收集时的堆和垃圾收集信息。比如,这是一个大型服务器应用的输出:


[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]



这里是两次小回收和之后的一次主回收。箭头前后的数字(比如第一行的325407K->83000K)分别指垃圾回收前后的所有活着的对象占用的空间。在小回收之后,这个尺寸之中仍然包含一些没有被回收的垃圾(死掉的对象)。这些对象要么存在在年老代中,要么被年老或永久代中的对象所引用。







后面的括号中的数字(比如第一行中的 (776768K))是全部提交的堆大小,也就是虚拟己不向操作系统申请内存的情况下,全部 java 对象可用的存储空间。注意,这个数字不包括幸存者空间中的一个,因为幸存者空间在一个给定时间只有一个可用,同时也不包括永久代的空间,这里面是虚拟机使用的元数据。







最后一个数字(比如 0.2300771 secs)是垃圾收集所用的时间;这个例子里大约是四分之一秒。







第三行中主垃圾回收的格式也是类似的。





-verbos:gc 输出的格式可能在将来的版本里有所改变。






通过-XX:+PrintGCDetails参数可以查看更多垃圾回收相关的信息。下面是串行垃圾收集器使用该参数打印出来的信息。


[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]


这个信息显示,这次小回收收回了 98% 的 DefNew 年轻代的数据,64575K->959K(64576K) 并在其上消耗了 0.0457646
secs
(大约45毫秒)。







整个堆的占用率下降了大约51% 196016K->133633K(261184K),而且通过最终的时间 0.0459067 secs 显示在垃圾收集中有轻微的开销(在年轻代之外的时间)。







选项-XX:+PrintGCTimeStamps会提供每次回收开始时间的时间戳。这对于查看垃圾回收频率非常有用。









111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505
secs]
111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs]  26282K->2311K(32704K), 0.1293306 secs]






如上,垃圾回收在程序运行后111秒开始。小回收同时启动。信息中还显示了主回收中的年老代的垃圾回收信息。年老代的空间使用率下降了大约 10% 18154K->2311K(24576K)
,用时 0.1290354(大约130毫秒)。






-verbose:gc 一样,-XX:+PrintGCDetails 的输出格式在将来的版本里也可能会有所变动。

4. 代的尺寸

很多参数会应想到代的尺寸。下图是堆中的提交空间和虚拟空间的差别。虚拟机初始化的时候,整个堆空间都是保留的。保留空间可以通过参数
-Xmx 指定。如果-Xms参数小于-Xmx参数,那么不是所有的保留空间都会立刻提交到虚拟机之中。未提交的空间在途中标记为 virtual。堆的不同部分(永久时间段、年老时间段和年轻时间段)可以按需生长到虚拟空间的限制为止。


一些参数可以调整堆的不同部分的比例,比如参数NewRatio指定年老代对年轻代的比例。这些参数将在下面讨论。







4.1 全部堆

注意,下面的关于堆的生长、收缩和缺省堆大小都不适用于并行垃圾收集器,并行垃圾收集器请参考相关章节。不过,用于控制整个堆大小和代尺寸的参数对并行垃圾收集器都是适用的。


因为垃圾收集是发生在代被填满的时候,所以,吞吐量反比于可用此内存数量。总可用内存数是影响垃圾收集性能的最重要因素。





缺省情况下,虚拟己在每次垃圾收集后增加或减少堆尺寸,来尽量保持可用空间对活的对象之间的比例在一个区间之内。这个目标区间通过参数-XX:MinHeapFreeRatio=<minimum>-XX:MaxHeapFreeRatio=<maximum>来设置,而总的堆大小的界限由-Xms<min>-Xmx<max>来确定。这些参数在 32 位 Solaris 系统(SPARC 平台版本)中的缺省值如下表所示:





























Parameter



Default Value


MinHeapFreeRatio 40
MaxHeapFreeRatio 70
-Xms 3670k
-Xmx 64m



64位系统中的堆尺寸的参数会大 30% 左右,这个增长用来补偿64位系统中更大的对象所带来的开销。





通过设置这些参数,当一个代的可用空间低于 40%,虚拟机就会把可用内存扩展到 40%,直到代的最大尺寸。同样的,如果可用空间超过 70%,代就会被缩小,使得只有 70% 可用空间,直到达到代最小的空间为止。


大型服务器程序在使用这些缺省设置时,经常遇到两种问题。其一是慢启动问题,初始的堆尺寸过小,经常需要经历多次主回收才能达到稳定值。另一个更现实的问题是,对于大多数服务器应用来说,这个缺省的最大堆大小太小了。对于服务器程序而言,设置的一般原则是:



  • 除非遇到了时延问题,给虚拟机尽量多的内存。缺省尺寸(64MB)通常都太小了。

  • 把-Xms 和 -Xmx 设置成相同的值,把最重要的尺寸决定从虚拟机收回来,从而增强可预见性。

  • 一般地,随着处理器数量的增加而增加内存,因为内存分配可以被并行化。
作为参考,有一个单独的页面会介绍各个命令行参数


4.2 年轻代

影响位居次席的是用于年轻代的堆比例。年轻代越大,小回收的次数也就越少。不过,在一定的堆大小的情况下,年轻代越大,年老代也就越小,这就增加了主回收的频率。最佳选择依赖于应用中分配的对象的生存期分布。





缺省的,年轻代的尺寸由 NewRatio 控制。比如,设置-XX:NewRatio=3意味着年轻代和年老代的比例是1:3。换句话说, eden 和幸存者空间的总和是整个堆大小的四分之一。





参数 NewSizeMaxNewSize 约束了年轻代的上下界限。可以把这两个参数设成相同的值来固定年轻代的大小,设置 -Xms 和 -Xmx 一样来设置堆大小为固定值。这样可以比使用NewRatio更细粒度地调整年轻代的大小。


4.2.1 幸存者空间



如果需要,SurvivorRatio 可以用来调整幸存者空间的大小,不过这对于性能一般影响不大。比如,-XX:SurvivorRatio=6 会设置幸存者空间和eden的比例是 1:6。换句话说,每个幸存者空间将是 eden 的六分之一,是整个年轻代空间的八分之一(不是七分之一,因为一共有两个幸存者空间)。


如果幸存者空间过小的话,拷贝收集到的幸存者将会直接溢出到年老代的空间中去。如果幸存者空间太大的话,他们也就是空着浪费掉。每次垃圾收集中,虚拟机会选择一个对象在成为年老的之前被复制的次数门限。这个门限的设置会保证幸存者空间是半满的。命令行参数-XX:+PrintTenuringDistribution 可以显示这个门限和年轻代中对象的年龄。这对于观测应用中对象的生存期分布也是有用的。




下面是 SPARC 上的 32 位 Solaris 的各个参数的缺省值,在其他平台上可能有所差异。








































Default Value



Parameter



Client JVM



Server JVM



NewRatio

8

2

NewSize

2228K

2228K

MaxNewSize

not limited

not limited

SurvivorRatio

32

32



年轻代的最大尺寸通过最大堆尺寸和 NewRatio 计算而得。所谓的“无限制”的缺省值是说这个计算的值不会受到 MaxNewSize 的约束,除非命令行中指定了这个值。




服务应用的设置准则是:



  • 首先确定可以提供给虚拟己的最大堆尺寸。然后根据性能需求来确定年轻代的尺寸,来找到最佳设置。
    • 注意:最大堆尺寸一定要小于系统中的内存数量,以防止过多的缺页错误和换页。

  • 如果总的堆尺寸是确定的,增加年轻代的尺寸就会减少年老代的尺寸。一定要保证年老代的尺寸,使之可以容纳所有在应用全程都要用到的活对象,并留有一定裕量(10-20%或更多)。
  • 依照上述年老代的约束:
    • 给年轻代分配足够的内存。
    • 如果有多个处理器,那么分配更多的内存给年轻代,因为内存分配可以并行化。



5. 可用的垃圾收集器

到目前为止,我们讨论的还都是串行垃圾收集器。不过 Java HotSpot 虚拟机一共支持了三种不同的收集器,每种提供不同的性能特性。
  1. 串行垃圾收集器使用单线程进行所有垃圾收集工作,因为没有线程间通信的开销,串行垃圾收集器相当高效。串行垃圾收集器最适合于单处理器系统,因为它不会从多处理器硬件中获益,尽管在小数据量的应用中(不大于100MB的),它对于多处理器系统也是游泳的。串行垃圾收集器在一定的硬件和操作系统的配置时会缺省使用,也可以显式地用 -XX:+UseSerialGC 参数来指定。
  2. 并行垃圾收集器(或吞吐垃圾收集器)并行进行小垃圾收集,这会显著减少垃圾收集的的开销。它适用于中等或大尺寸数据的运行在多处理器或多线程硬件上的应用。并行垃圾收集器也会在一定的硬件和操作系统配置下被缺省使用,同时,也可以使用 -XX:+UseParallelGC 参数来指定。
    • 更新:“并行压缩”是 J2SE 5.0 update 6 以上版本的新特性,并在 Java SE 6 之中得到加强,该特性允许主回收也并行收集。如果不使用并行压缩,主回收仍然会单线程运行,这会严重限制系统的可伸缩性。并行压缩可以使用命令行参数-XX:+UseParallelOldGC 来打开。

  3. 并发垃圾收集器并发地进行大部分垃圾收集工作(也就是在应用运行当中进行)来尽可能煎炒垃圾收集带来的应用停顿。它是为哪些拥有中到大量数据的、对响应时间要求高于吞吐量要求的应用,因为最小化时延的技术会让吞吐能力付出代价。并发垃圾收集器通过 -XX:+UseConcMarkSweepGC 参数来启用。

5.1 选择垃圾收集器

除非你的应用有非常严酷的时延要求,那么就运行你的应用,并让系统自己选择垃圾收集器好了。如果有必要的话,就调整堆的大小来增进性能。如果性能仍然无法达到你的目标,那就按照如下设置来选择一个垃圾收集器。
  1. 如果应用的数据很少(大约不超过100MB),那么

    • 使用-XX:+UseSerialGC选择串行垃圾收集器。


  2. 如果应用运行在单处理器系统中,并且没有什么时延要求,那么
    • 让虚拟机选择垃圾收集器,或者

    • 使用-XX:+UseSerialGC选择串行垃圾收集器。

  3. 如果(a)程序峰值性能是第一位的,并且(b)没有时延要求,或时延要求是一两秒或更长,那么

    • 让虚拟机选择垃圾收集器,或者
    • 使用-XX:+UseParallelGC选择并行垃圾收集器,乃至(可选)通过 -XX:+UseParallelOldGC启用并行压缩。

  4. 如果响应时间比总体吞吐量更为重要,并且垃圾收集时延需要控制在1秒以内,那么

    • select the concurrent collector with
      -XX:+UseConcMarkSweepGC. If only one or two
      processors are available, consider using

      incremental mode,

      described below.
    • 通过 -XX:+UseConcMarkSweepGC 参数启用并发垃圾收集器。进当你有一个或两个处理器可用的时候,考虑使用下文将要介绍的“增量模式”。


这些指导意见仅仅是选择垃圾收集器的起点,因为性能依赖于堆的尺寸、应用中活数据的数量,以及处理器的数量和速度。时延参数对这些因素尤为敏感,所以,所谓的1秒门限值只是个大致数值:在很多硬件和数据量的组合情况下,并行垃圾收集器可能会导致停顿时间超过1秒;同样,在某些组合下,并发垃圾收集器也不能保证停顿小于1秒。




如果推荐的垃圾收集器没有达到期望的性能,首先应该尝试堆和代的尺寸,以期达到目标。如果仍然不成功的话,尝试更换一个垃圾收集器:使用并发垃圾收集器来减少停顿时间,使用并行垃圾收集器来增加多处理器系统中的吞吐量。


6. 并行垃圾收集器

并行垃圾收集器(也被称为吞吐量收集器)和串行收集器类似,也是一种分代垃圾收集器;其最大的不同在于它使用了多线程来加快垃圾收集的过程。并行垃圾收集器可以通过参数 -XX:+UseParallelGC 指定。缺省的,只有小回收会并行运行,主回收仍然单线程运行。不过,通过参数-XX:+UseParallelOldGC启动并行压缩可以让主回收和小回收都并行运行,从而进一步减少垃圾收集开销。




在一个有N个处理器的计算机上,并行垃圾收集器使用N个垃圾收集器线程。不过,这个数量可以在命令行参数里指定(参见下文)。在一台单处理器的计算机上,由于线程开销(比如同步),并行垃圾收集器的性能应该不如串行垃圾收集器。然而,当应用程序有中等或大尺寸的堆的时候,它在一个双处理器的机器上就会略优于串行垃圾收集器,而如果有多于两个处理器的话,它就能远胜于串行垃圾收集器。





垃圾收集器线程数的多少可以用-XX:ParallelGCThreads=<N>参数来控制。如果要使用命令行参数显式调整了堆的尺寸,使用并行垃圾收集器的情况下需要的堆的尺寸和使用串行垃圾收集器情况下的堆的尺寸是一阶相等的。使用并行垃圾收集器仅仅是让小回收造成的停顿更短一些。因为有多个垃圾收集器线程参与小回收的过程,有极少的可能性可能会在将年轻代移动到年老代的过程中造成一些碎片。每个垃圾收集线程都有一块专属的年老代的空间,用于年轻代向年老代的移动,将年老代的可用空间划分为“移动缓冲”(promotion buffer)的过程可能会造成一定的碎片效应。减少垃圾收集器线程的数量可以减少碎片、增加年老代的空间。


6.1 代

正如上面提到的,并行垃圾收集器的代的排布方式和串行垃圾收集器略有不同。其分布如下图所示。

6.2 功效学

自 J2SE 5.0 以来,并行垃圾收集器成为了server级机器的缺省垃圾收集器,详细资料可以参考“Garbage Collector
Ergonomics
”。此外,并行垃圾收集器使用一种自动调整机制来指定期望的行为而不是指定代的大小和其他底层调整细节。这些行为包括:


  • 最大垃圾收集停顿时间
  • 吞吐量
  • Footprint (也就是堆尺寸)



最大停顿时间的目标由参数-XX:MaxGCPauseMillis=<N>来指定。这个参数被解释为指定停顿时间不得大于<N>毫秒;缺省情况下没有最大停顿时间目标。如果指定了一个停顿时间目标,堆尺寸和其他垃圾回收相关参数就会被相应调整,以便保持垃圾回收时间小于指定的值。注意,这些调整可能会导致总体吞吐量的降低,而且,在某些情况下,要求的停顿时间目标可能无法达到。





吞吐量目标测量垃圾回收时间和非垃圾回收时间(也就是应用时间)的比例。这个目标时间可以用命令行参数-XX:GCTimeRatio=<N>来指定,这样,垃圾回收时间和应用时间的比例将是1 / (1 + <N>)。例如,-XX:GCTimeRatio=19设置1/20活5%的时间用于垃圾回收。缺省值是99,目标是1%的时间用于垃圾回收。



最大堆footprint使用已经存在的 -Xmx<N> 参数。此外,如果没有其他的优化目标的话,垃圾收集器有一个隐式的最小化堆尺寸的目标。


6.2.1 目标的优先级

目标的优先级顺序如下:



  1. 最大停顿时间目标
  2. 吞吐量目标
  3. 最小堆尺寸目标



最大停顿时间目标会被首先满足。仅当最大停顿目标被满足的情况下,才会去满足吞吐量目标。类似的,仅当前两个目标都会满足的情况下,才会考虑去满足footprint目标。


6.2.2 时间段尺寸调整


每次垃圾收集结束的时候,垃圾收集器都会更新其保存的平均停顿时间之类的统计参量。同时它会检查各个目标是否被满足了,是否有调整代尺寸的需要。这之中的意外情况就是显式的垃圾收集(比如调用 System.gc())会在统计和调整判断中被忽略掉。





增加和缩小一个代的大小是通过增加活缩小一个固定的百分比来达到的,这样一个代要分步来达到需要的尺寸。增加活所见是以不同的比率来进行的。缺省情况下,一次增加 20% 活减少 5%。年轻代和年老代增量的比例分别通过命令行参数 -XX:YoungGenerationSizeIncrement=<Y>-XX:TenuredGenerationSizeIncrement=<T>来设定。而缩小比例的要通过-XX:AdaptiveSizeDecrementScaleFactor=<D>参数来设定。如果增量是X%,那么每次减小量就是(X/D)%。





如果垃圾收集器决定在启动的时候增加一个代的大小,会有一个额外的百分比的增量。这个附加的增量随着收集的次数而减少,不会长期影响。这个额外增量意在提高启动速度。缩小代的尺寸是没有这个额外的增量。




如果最大停顿时间目标没有达到,会有且仅有一个代的大小被缩小。如果两个代都在目标之上,停顿时间较大的那个代会首先被缩小。





如果总体吞吐量目标没有达到,那么两个代的大小都会增加。每个都按照各自对垃圾回收时间的贡献比例分别增加。比如,如果年轻代的垃圾回收时间占去了25%的总垃圾回收时间,并且年轻代的全部增量应该是20%,那么这时它的增量就是5%。


6.2.3 缺省堆尺寸

如果没有在命令行中进行设置,初始和最大堆尺寸会通过计算机内存计算而得。如下表所示,对大小占用的内存的比例是由参数
DefaultInitialRAMFractionDefaultMaxRAMFraction来控制的。(表中的 memory 代表计算机的系统内存数量。)

























Formula
Default

initial heap size

memory / DefaultInitialRAMFraction

memory / 64

maximum heap size

MIN(memory / DefaultMaxRAMFraction, 1GB)

MIN(memory / 4, 1GB)




注意,缺省的最大堆尺寸不会超过1GB,不论系统中到底有多少内存。


6.3 过多的GC时间和OutOfMemory错误

当有过多的时间花费在垃圾收集上的时候,并行垃圾收集器会跑出 OutOfMemoryError 错误:如果超过 98% 的时间花费在垃圾收集上并且只有 2% 的堆被释放的话,就会抛出一个 OutOfMemory。这个功能是用来防止堆太小导致程序长时间无法正常工作而设计的。如果必要,这个功能可以使用命令行参数-XX:-UseGCOverheadLimit来关闭。

6.4 测量

并行垃圾收集器的垃圾收集器详细输出和串行垃圾收集器是一样的。

7. 并发垃圾收集器

并发垃圾收集器适用于那些需要更短的垃圾收集停顿,并能为此付出程序运行期处理器资源的应用。典型情况下,那些拥有较多长期存在的对象(年老代比较大),并且运行在拥有两个活更多处理器的应用可能会因此获益。不过,在任何要求很低停顿时间的应用都应该考虑这个垃圾收集器;比如,拥有较小年老代的交互程序在但处理器上使用并发垃圾收集器就可以收到明显的好处,特别是使用增量模式的时候。并发垃圾收集器可以通过命令行参数-XX:+UseConcMarkSweepGC来启动。


和其他垃圾收集器类似,并发垃圾收集器也是分代的;所以也有小回收和主回收。并发垃圾收集器通过使用独立的垃圾收集线程于应用本身的线程并发执行跟踪所有可及的对象,以期降低主回收导致的停顿。在每个主回收周其中,并发垃圾收集器会在垃圾收集的开始让所有应用线程暂停一下,并在回收中段再暂停一次。第二次暂停相对而言会更长一些,在此期间会有多个线程来进行收集工作。剩下的收集工作包括大部分的活对象跟踪和清除不可及的对象的工作都由一个或多个和应用并发的垃圾收集器线程来进行。小回收会在进行的主回收周其中穿插进行,其模式和并行垃圾收集器十分类似(特别需要说明的就是,在小回收期间,应用线程是会有停顿的)。





并发垃圾收集器的基本算法在技术报告
A Generational
Mostly-concurrent Garbage Collector
里有介绍。主义,实际的实现细节在不同版本里手有细微的变化的,因为垃圾收集器也在一直进步。

7.1 并发性的开销

并发垃圾收集器的短主回收停顿时间是以处理器资源作为代价的(这些资源如果不用在收集器上肯定就要用在应用上了)。最明显的开销就是并发地使用了一个或多个处理器资源。在N处理器系统中,垃圾收集的并发部分会使用K/N的可用处理器,其中 1<=K<=ceiling{N/4}。(注意,K值的上限将来可能会有变化。)并发垃圾收集器不仅在并发阶段使用处理器,还引入了其他的开销。所以,尽管并发垃圾收集器显著减少了程序的停顿,但和其他垃圾收集器相比,应用的总体吞吐量会受到轻微的影响。


在拥有多个处理器的计算机上,在并发垃圾收集器运行的时候,应用程序仍然能使用到CPU,所以,并发垃圾收集器并没有让程序停顿。这通常意味着更短的停顿,谈也意味着更少的应用可用的处理器资源,并且让它运行得相对比较慢,特别是当应用可以完全的利用多个CPU核心的时候更是如此。随着N的上升,垃圾收集器导致的损失会相对变小,而从并发垃圾收集的获益则相对提高。下一节“并发模式失败”会讨论这种规模扩张的潜在局限。





因为在并发阶段至少有一个处理器用于了垃圾收集,所以在单处理器(单核)系统中,并发垃圾收集器一般不会带来什么好处。不过,并发垃圾收集有一个分离模式可以在单处理器或双处理器系统中显著减少停顿时间;后面的增量模式中将会进一步介绍其细节。

7.2 并发模式失败

并发垃圾收集器使用一个或多个垃圾收集线程在应用线程执行的同时运行,从而在年老代和永久代变满之前就完成垃圾收集。如前文所述,在一般的操作中,并发垃圾收集器的大部分跟踪与清理工作是在程序运行的同时进行的,所以,程序线程只有极少的停顿。但是,如果并发垃圾收集器在年老代变满的时候仍没有完成垃圾清除工作,或是年老代中的可用空间无法满足一次分配操作的需要的时候,应用就不得不被暂停下来以等待应用线程结束了。这种无法并发地完成垃圾收集的情况被称为“并发模式失败”,这就需要对并发垃圾收集器的参数进行调整了。

7.3 过多的GC时间和OutOfMemory错误


并发垃圾收集器会在垃圾收集消耗时间过多的时候抛出 OutOfMemoryError 错误:如果多于 98% 的时间被花费在了垃圾手机上,并且仅有少于 2% 的堆被回收的话,就会抛出 OutOfMemoryError。这个功能是用来防止堆太小导致程序长时间无法正常工作而设计的。如果必要,这个功能可以使用命令行参数-XX:-UseGCOverheadLimit来关闭。




这个策略和并行垃圾收集器是基本一致的,惟一的区别就是并发的垃圾收集时间并未计算在内。也就是说,只有哪些程序停顿下来进行垃圾收集的时间才被计算在内了。这些垃圾收集常常是由于并发模式失败或是显式垃圾收集请求(如调用 System.gc())导致的。

7.4 浮动垃圾

并发垃圾收集器与 HotSpot 中的其他垃圾收集器一样,是一种识别至少所有在堆中可以被访问到的对象的跟踪收集器。按照Jones and Lins的说法,是一种增量更新(Incremental Update)垃圾收集器。因为应用现成和垃圾收集器线程在主回收过程中并发执行,那么那些垃圾收集器跟踪的对象就可能在垃圾收集完成之后变成垃圾这些无法访问却还没有被回收的对象被称为浮动垃圾(floating garbage)。浮动垃圾的数量取决于垃圾收集周期的长度和程序中引用更新的频率,也被称为转化率(mutation)。而且,另一个原因是年轻代和年老代的收集是独立的,彼此都是对方的根。一个粗略的配置规则是为年老代的浮动垃圾多预留出20%的空间来。一个垃圾回收周期中的堆中的浮动垃圾会在下一个垃圾回收周期中被回收。

7.5 时延(停顿)

并发垃圾收集器在一个并发回收周期中会两次暂停应用。第一次会从根从根(比如从对象线程栈和寄存器、静态对象等的引用)和堆的其他部分(如年轻代)开始标记所有直接可达的活的对象。第一次停顿被称为“初始标记停顿”(initial mark pause)。第二次停顿发生在并发跟踪阶段末尾,用来发现由于在垃圾收集线程跟踪完一个对象之后又被应用线程更新了其引用而没有被并发跟踪到的对象。这次停顿被称为“重标记停顿”(remark pause)。

7.6 并发阶段

可达对象的并发跟踪图发生在初始标记停顿和重标记停顿之间。在并发跟踪阶段中,一个或多个并发垃圾收集器线程会使用那些本来可能会被应用使用的处理器资源,所以尽管不会停顿,计算密集型应用可能会在此阶段和其他并发阶段受到相当的吞吐量损失。在重标记停顿之后,还有一个并发清理阶段,会收集所有标记为不可达的对象。一旦手机周期结束了,并发收集器就会进入等待阶段,这时就基本不会消耗任何计算资源了,直到下一个主回收周期开始为止。

7.7 开始并发收集周期

在串行收集器中,每当年老代满了的时候都会引发一次主回收,所有应用现成都会在主回收期间暂停运行。并发垃圾收集器与之不同,它需要在足够早的时间开始垃圾收集,以便能在年老代变满之前完成垃圾收集;否则的话就会因为并发模式失败而导致较长的时延。有很多种条件可以触发并发垃圾收集器启动。


基于最近的历史记录,并发垃圾收集器维护了一个年老代变满的预期剩余时间和一个垃圾收集周期的预期时间。基于这些动态估计,并发垃圾收集周期会以让垃圾收集周期在年老代变满之前完成为目标开始并发垃圾收集周期。因为并发模式失败的代价非常惨重,这些估值都流出了安全裕量。





并发垃圾收集在年老代的已用百分比超出了一个初始占有率值(initiating occupancy)的时候也会启动。这个初始占有率阈值的缺省值大约是 92%,不过这个值可能在不同版本中略有不同。它也可以通过命令行参数-XX:CMSInitiatingOccupancyFraction=<N> 来手工设置,其中N是一个0-100的整数,代表年老代的占用百分比。

7.8 调度中断

年轻代和年老代的垃圾收集的停顿发生彼此间是独立的。他们不会重合,但可能会连续发生,这样也就让一个垃圾收集的停顿连上下一个垃圾收集的停顿了,从外界来看就是一个长停顿了。为了避免这种情况,并发垃圾收集器会调度重标记停顿的时间,使之发生在前后两个年轻代停顿之间。这个调度目前还不应用于初始标记停顿,因为它通常会比重标记停顿短很多。

7.9 增量模式

并发垃圾收集器可以在这样一种模式下工作:并发阶段以增量的方式进行。回忆一下,在并发阶段,垃圾回收线程会使用一个或多个处理器。所谓增量模式是指减少长并发阶段的影响,周期性中断并发阶段,将处理器资源还给应用程序。这种模式又称为“i-cms”,将垃圾收集器的并发工作划分到小块时间,在年轻代垃圾收集之间进行。这个功能对于那些工作在没那么多处理器的机器上(1或2个处理器的)需要并发垃圾收集器的低时延应用非常有用。




并发垃圾收集周期通常包括如下几步:




  • 停止所有的应用线程,标记从根开始可达的对象集,然后继续所有的应用线程
  • 在应用线程运行的同时,使用一个或更多的处理器,并发跟踪可达的对象图
  • 使用一个处理器,并发跟踪对象图中在上一步开始之后的各个改动的部分
  • 停止所有的应用线程,重新跟踪根和对象图中自从上次检查开始发生了变化的部分,然后继续运行线程
  • 使用一个处理器,并发地把不可达对象清理到用于分配空间的 free list 上面去。
  • 使用一个处理器并发地调整堆的大小,准备下一个回收周期所需的数据结构




正常情况下,并发垃圾收集器在并发跟踪阶段使用一个或多个处理器,不会让出它们。类似的,在清理阶段也会始终独占地使用一个处理器。这对于对于一个程序的响应时间可能是个不小的影响,特别是系统中只有一两个CPU的时候。增量模式通过将并发阶段分解为一系列的突发行为来降低这一影响,这些突发行为会散布在小回收之间。


i-cms 使用占空比来控制并发收集器自发的放弃处理器之前的工作量。占空比是年轻代收集之间的允许并发垃圾收集器运行时间的百分比。i-cms 可以根据应用的行为自动计算占空比(这也是推荐的方法,称为自动步长(auto pacing)),当然,也可以通过命令行指定一个固定的值。

7.9.1 命令行参数


下面是控制 i-cms的命令行参数(参考下文的初始设置建议):



















































参数
描述
缺省值

J2SE 5.0 及以前

Java SE 6 及以后

-XX:+CMSIncrementalMode
启动增量模式。注意,并发垃圾收集器必须也被选择(-XX:+UseConcMarkSweepGC) ,否则此参数无效。
disabled disabled
-XX:+CMSIncrementalPacing
打开自动步长,这样,增量模式占空比将根据JVM统计到的信息自动调整。
disabled enabled
-XX:CMSIncrementalDutyCycle=<N>
两次小回收之间的允许并发收集器运行的时间的百分比(0-100)。如果打开自动步长,那么这个值就是初始值。
50 10
-XX:CMSIncrementalDutyCycleMin=<N>
自动步长打开后,占空比值的下限 (0-100)。
10 0
-XX:CMSIncrementalSafetyFactor=<N> 计算占空比值时使用的一个裕量(0-100)
10 10
-XX:CMSIncrementalOffset=<N>在小回收之间,增量模式中占空比开始的时间,或说是向右的平移量(0-100)0
0
-XX:CMSExpAvgFactor=<N>
当进行并发回收统计,计算指数平均值时,当前采样所用的权值(0-100)
25
25


7.9.2 建议参数


要在 Java SE 6 里使用 i-cms,需要使用如下命令行参数

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
前两个参数分别启动并发垃圾收集器和 i-cms。后两个参数不是必须的,它们只是要求垃圾收集器将诊断信息打印到标准输出,这样,垃圾收集器的行为就可以被看到并用于以后分析了。



注意,对于 J2SE 5.0 和之前的版本,我们建议 i-cms 使用如下的初始命令行参数:

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps \
-XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0
-XX:CMSIncrementalDutyCycle=10


这样,就是用了和 Java SE 6 一致的参数了,多出的三个参数用于自动调整占空比。这些多余的参数值完全是使用的 Java SE 6 的缺省值。


7.9.3 基本问题处理

i-cms 的自动占空比计算模式使用了程序运行时收集到的统计信息进行占空比计算,以保证并发垃圾收集器可以在堆占满之前完成。不过,使用过去的行为预测将来的变化的估计方式可能并不总是足够准确,可能在某些情况下无法阻止堆用满。如果需要收集的垃圾太多,可以尝试下面这些步骤,一次使用一个:






















Step Options

1. 增加保险系数


-XX:CMSIncrementalSafetyFactor=<N>


2. 增加最小占空比


-XX:CMSIncrementalDutyCycleMin=<N>


3. 关闭自动占空比计算,使用固定占空比


-XX:-CMSIncrementalPacing
-XX:CMSIncrementalDutyCycle=<N>

7.10 测量



下面是使用-verbose:gc和-XX:+PrintGCDetails参数时,并发垃圾收集器的输出,一些小细节已经被去掉了。注意,并发垃圾收集器的输出里掺杂着小回收的输出;典型情况下,很多小回收会发生在并发收集周期之中。其中的CMS-initial-mark表征了一个并发垃圾回收周期的开始。CMS-concurrent-mark:
标志着并发标记阶段的完成,而CMS-concurrent-sweep则标志着并发清除阶段的完成。之前没有提到过的预清除阶段以CMS-concurrent-preclean为标志。预清除可以和重标记阶段CMS-remark的准备工作同时运行。最后一个阶段是CMS-concurrent-reset,这是下一个并发收集周期的准备工作。




[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs]
[GC [DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]
...
[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs]
[CMS-concurrent-mark: 0.267/0.374 secs]
[GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs]
[CMS-concurrent-preclean: 0.044/0.064 secs]
[GC [1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs]
[GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs]
[GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]
...
[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs]
[CMS-concurrent-sweep: 0.291/0.662 secs]
[GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs]
[CMS-concurrent-reset: 0.016/0.016 secs]
[GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]



初始标记停顿在典型情况下比小回收的停顿时间还要小。而如上例所示,并发阶段(并发标记、并发预清除和并发清除)通常会比小回收长很多。不过注意,应用并没有在这些并发阶段中停顿下来。重标记停顿通常和一个小回收的长度相当。重标记停顿挥手道应用的某些特征(如高对象修改频率可能会增加这个停顿)和上一次小回收的时间(即,更多的年轻代对象可能会增加这个停顿)的影响。

8. 其他考虑

8.1 永久代尺寸



在大部分应用中,永久代对于垃圾回收性能没有显著的影响。不过,一些应用会动态的生成与加载很多类;比如,一些 JavaServer Pages(JSP)页面的实现。这些应用可能需要很大的永久代去存放一些多余的类。如果这样的话,最大永久代的尺寸可以用命令行参数-XX:MaxPermSize=<N>来增大。


8.2 Finalization; Weak, Soft and Phantom References

一些应用使用 finalization 和 weak, soft, phantom 引用与垃圾收集器交互。这些特征可以 Java 语言层带来性能影响。一个例子是通过 finalization 来关闭文件描述符,这会导致一个外部资源依赖于垃圾收集器。以来垃圾收集器来管理内存之外的资源是个坏主意。

参考资料章节中的文章深度讨论了一些finalization的常见错误和用来避免这些错误的技术。


8.3 显式垃圾回收

应用程序和垃圾回收器的另一个交互途径是显式调用 System.gc() 进行完整的垃圾回收。这回强制进行一次主回收,即使没有必要(也就是说一次小回收可能就足够了),所以应该避免这种情况。显式垃圾回收对性能的影响可以通过使用 -XX:+DisableExplicitGC 进行比较来进行测量,这样虚拟机会无视 System.gc() 的。



最常见的显式调用垃圾回收的场景是 RMI 的分布式垃圾回收。使用 RMI 的应用会引用到其他虚拟机中的对象。在这种分布式应用的场景下,本地堆中的垃圾可能不能被回收掉,所以 RMI 会周期性强制进行完整的垃圾回收。这些回收的频率可以使用参数来控制。如
java -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 ...



这里指定了垃圾回收每小时运行一次,而不是缺省的每分钟一次。不过,这可能会导致某些对象的清除消耗太长时间。这些参数可以被设置到高达Long.MAX_VALUE来让显式垃圾回收的间隔时间无限长,如果没有合适的DGC上限时间的话。

8.4 Soft References




Soft reference在虚拟机中比在客户集中存活的更长一些。其清除频率可以用命令行参数
-XX:SoftRefLRUPolicyMSPerMB=<N>来控制,这可以指定每兆堆空闲空间的 soft reference 保持存活(一旦它不强可达了)的毫秒数,这意味着每兆堆中的空闲空间中的 soft reference 会(在最后一个强引用被回收之后)存活1秒钟。注意,这是一个近似的值,因为 soft reference 只会在垃圾回收时才会被清除,而垃圾回收并不总在发生。



8.5 Solaris 8 替换 libthread




The Solaris 8 Operating System supports an alternate version of the threading
library, libthread, that binds threads to light-weight processes (LWPs)
directly. Some applications can benefit greatly from the use of this alternate
libthread and it is a potential benefit for any threaded application. The
following commands will load the alternate libthread for java (Bourne shell
syntax is shown):

Solaris 8 操作系统提供了一个替代的线程库,libthread, 它将线程直接绑定成了轻量级进程(LWP)。有些应用能够从中极大获益,并潜在的对所有多线程应用都或多或少的有好处。下面的命令会为 java 启用替换的 libthread(BASH 格式)

LD_PRELOAD=/usr/lib/lwp/libthread.so.1
export LD_PRELOAD
java ...



这个方法仅对 Solaris 8 适用,因为对 Solaris 9 操作系统来说,这是缺省的,而 Solaris 10 中,这是惟一的线程库。


9. 相关资源


  1. HotSpot VM Frequently
    Asked Questions (FAQ)


  2. GC output
    examples
    介绍了如何解释不同垃圾收集器的输出。


  3. How to Handle
    Java Finalization's Memory-Retention Issues
    介绍了一些容易犯的错误和避免他们的方法。


  4. Richard Jones and Rafael Lins, Garbage Collection: Algorithms
    for Automated Dynamic Memory Management, Wiley and Sons
    (1996), ISBN 0-471-94148-4





在本网站中,名词“Java Virtual Machine” 和“JVM” 都代表 Java 平台虚拟机。



Sunday, July 01, 2007

用 aptitude 处理 debian 软件包的依赖关系

(内容并不超出 aptitude 文档的范围,这里用以提示给没有阅读完整文档的用户)

在使用 aptitude 安装、删除、升级软件包的时候,有时会遇到依赖关系问题,通常可以利用 aptitude 的自动解决功能,但有的时候,我们希望能手工选择一下如何处理依赖关系。

当在软件包界面进行选择时,在出现依赖关系问题时,会在界面下方得到红色提示,这时,按 e ,会得到依赖关系解决方案界面,里面列出了一个解决方案,包含安装、升级、降级、保持、删除若干个软件包,上下移动光标,会发现下方的区域会列出每个软件包可以用来解决该问题的几种方案。

这时可以用 ``.'' 和 ``,'' 前后翻阅不同的解决方案,翻到哪,再按 g 就会执行哪个方案了。

当然,我们还可以更精确的选择,解决方案界面,对哪个软件包按 a 就表示接受对这个软件包的这种处理方式,这样,就极大地限制了备选方案的数量,同理,对软件包按 r 就可以拒绝一个软件包的处理方式。结合 a/r/./, 可以更快地找到合适的解决方案。
fdupes: 发现文件的重复拷贝

在使用了一段时间的计算机之后,很多文件都可能被保存或复制了很多份,而且常常是在无意状态下造成这些副本的,你是不是想把他们找出来并删掉呢?

原则上讲,这个工作并不难,但是比较复杂,需要列出所有文件,比较大小乃至 md5,因为同名文件不一定有相同的内容,尤其是对于 README 这样的文件名,而且相同的文件不一定名称也一样。

fdupes 就是这样一个有用的小工具,它能很高效率地完成这一工作,用法很简单: 在 fdupes 后面加上要查看的所有目录,同时,还有一些开关:
  • -n 不比较所有大小为0的文件
  • -r 递归搜索所有子目录
其它功能参考 man page 吧 :)

Sunday, June 17, 2007

[近期译文] GIT 简要教程

git 简要教程 (适用于 1.5.1 或更新版本)

原文链接: http://www.kernel.org/pub/software/scm/git/docs/tutorial.html


这个教程将介绍如何将一个新的项目导入到 git 之中,如何修改项目并如何将这些变更与其他开发者分享。

如果你更感兴趣如何用 git 取出一个项目,比如,测试软件的最新版本,你可能更应该看看 The Git User's Manual 的前两章。

首先,记住你可以用 man 来获取 git 的文档,比如 "git diff" 的文档可以用如下命令察看:

$ man git-diff

在做任何改动之前,最好把自己的名字和 email 地址介绍给大家,最简单的方法就是:

$ git config --global user.name "Your Name Comes Here"
$ git config --global user.email you@yourdomain.example.com

导入一个新项目

假设你有一个名为 project.tar.gz 的 tarball 作为项目的初始内容。你可以如下操作来把它至于 git 版本控制之下。

$ tar xzf project.tar.gz
$ cd project
$ git init

Git 将会如下回复:

Initialized empty Git repository in .git/

现在,你已经初始化了工作目录——你可能已经注意到了名为 ".git" 的一个新目录了。下一步就是使用 git-add(1) 命令告诉 git 当前目录的所有文件全是项目的一个快照:

$ git add .

这个快照目前存放在一个临时区域之中,在 git 中称为 "index"。使用 git-commit 命令,你可以把 index 的所有内容永久性地存放到软件仓库之中:

$ git commit

这条命令会向你提示输入版本变更信息。这样,你的项目的第一个版本就已经存入 git 之中了。

进行修改

修改一些文件之后,你可以将

更新这些内容到 index 之中:

$ git add file1 file2 file3

现在,你已经准备就绪,可以提交了。现在你可以使用 git-diff(1) 命令的 --cache 参数

来查看都有哪些内容将会被提交:

$ git diff --cached

(如果不使用 --cached 参数,git-diff(1) 会显示所有还没添加进 index 的已经做出的改动。) 你也可以使用 git-status (1) 来获得一些当前状况的概要信息:

$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file1
# modified: file2
# modified: file3
#

如果你需要进行更多改动,现在就可以进行,然后可以添加到 index 之中。最后,使用如下命令提交改动:

$ git commit

这将再次要求你输入关于这次改动内容的描述性信息,之后记录下

的项目新版本。此外,如果想省掉提交之前的 git add 命令,你可以直接用

$ git commit -a

这样会自动检测所有修改过的文件 (不包括新文件) ,并一气呵成地将它们添到 index 之中,并提交。

关于提交的描述信息: 虽然这个信息不是必须的,但提交信息描述最好以一行不超过50个字符的概要性信息来开头,在一个空行之后再进行更多的描述。比如那些将 commit 转化为 email 的工具就会把这个第一行作为邮件标题,其余的提交内容则放在邮件内部。

Git 跟踪内容而不是文件

很多版本控制系统提供了一个 "add" 命令用来记录一个新文件。而 git 的 "add" 命令更加简单也更加强大: git-add 既用于新文件也用于新近改动的文件,在所有这些情况下,它在 index 中对所有的文件与状态进行一次快照,这样就可以在下一次 commit 命令中进行提交。

查看项目历史

在任何时候,你都可以如下查看所有你进行过的改动

$ git log

你可能还想看到每一步改进中的所有完整的 diff ,这可以使用如下命令

$ git log -p

浏览改动的概要对于获得每一步修改的情况常常是比较有用的,可以使用如下命令

$ git log --stat --summary

管理分支

一个 git 仓库可以包含多个开发分支。使用如下命令可以建立一个称为 "experimental" 的新分支

$ git branch experimental

如果你运行命令

$ git branch

你将可以得到类似下面的已有分支的列表

  experimental
* master

"experimental" 就是你刚刚建立的那个分支,而 "master" 分支则是建立仓库的时候自动创建的缺省分支,里面的星号表示你当前所在的分支;输入命令

$ git checkout experimental

就可以切换到 experimental 分支。现在修改一个文件,并提交改变,然后重新回到 master 分支:

(edit file)
$ git commit -a
$ git checkout master

你会发现,刚才的变更已经不可见了,这是因为这个改变是发生于 experimental 分支的,而你现在已经回到 master 分支了。

(edit file)
$ git commit -a

这里,两个分支已经产生不同了,每个分支上都发生了不同的改动。要把 experimental 中的改变也合并到 master 之中,运行命令

$ git merge experimental

如果两者的改变并不冲突,那么就算是完成了。而如果这里有冲突,有问题的文件左边会显示出标记,以表明这个文件发生了冲突;

$ git diff

上述命令将会列出具体的冲突。一旦你编辑文件解决了冲突,

$ git commit -a

这个命令将把合并的结果提交。最终,

$ gitk

会显示出漂亮的图标以展示历史变革。



这里你可以使用如下命令删除 experimental 分支。

$ git branch -d experimental

这个命令会确定 experimental 中的所有改动已经在当前分支当中了。


如果你在 crazy-idea 分支中进行开发,然后又后悔了,你可以用如下命令删除分支
$ git branch -D crazy-idea

分支操作十分简单而且代价低廉,所以适合于尝试一些东西。

使用 git 进行协作

假设 Alice 在 /home/alice/project 中的 git 仓库启动了一个新项目,而在本机中也拥有 home 目录的 Bob 想要贡献一些代码。


他可以以如下工作开始:

$ git clone /home/alice/project myrepo

这会新建一个名为 "myrepo" 的目录,里面包含了 Alice 的仓库的一份克隆。这份克隆与原始项目完全一致,可以处理自己的一份原始项目历史。


之后,Bob 进行了一些变更并提交了这些变动:

(edit files)
$ git commit -a
(repeat as necessary)

当他完成的时候,他告诉 Alice 将 /home/bob/myrepo 之中的变动导入到原始仓库之中。她使用如下命令来完成这一工作:

$ cd /home/alice/project
$ git pull /home/bob/myrepo master

这回合并 Bob 的 "master" 分支到 Alice 的当前分支。如果 Alice 也已经修改了某些内容,她需要手工修复冲突。(注意,"master" 参数实际上并不是必要的,因为这是缺省分支。) "pull" 命令包括两个操作: 从远端分支中取出改动,然后合并到当前分支之中。


当你只在一个很小的小组里工作的时候,通常不会频繁地访问同一个仓库。通过定义仓库的快捷方式,可以让访问远程仓库更方便一些:

$ git remote add bob /home/bob/myrepo

这样,你可以如下用 "git fetch" 命令仅取出改动,而不把它们合并到当前分支之中:

$ git fetch bob

和长格式不同,当 Alice 使用 git remote 设置的快捷方式从 Bob 的仓库中获取内容的时候,取出的内容存储在一个 remote tracking 分支之中,在本例中是 bob/master。所以,如下操作

$ git log -p master..bob/master

将会列出从 Bob 从 Alice 的主分支中分支出去以后的所有改动。

$ git merge bob/master

这个合并也可以通过从自己的 remote tracking 分支中 pull 来做到,如

$ git pull . remotes/bob/master

注意,git pull 总是合并进当前的分支,不论命令行给出的是什么。


之后,Bob 可以如下使用 Alice 的最近改动更新自己的仓库

$ git pull

这里,他不需要给出 Alice 的仓库的位置;当 Bob 克隆了 Alice 的仓库的时候,git 在仓库设置中保存了她的仓库的位置,即 pull 所使用的位置:

$ git config --get remote.origin.url
/home/bob/myrepo

(git-clone 创建的完整配置信息可以用 "git config -l" 获得,git-config(1) 的 man page 解释了所有选项的含义。)


Git 也在 "origin/master" 分支保存了一份 Alice 的主分支的原始拷贝:

$ git branch -r
origin/master

如果其后 Bob 决定转到另一台主机上工作,他还可以通过 ssh 来克隆原始仓库:

$ git clone alice.org:/home/alice/project myrepo

此外,git 本身也有远程协议,并且可以使用 rsync 或 http,详细情况可以查看 git-pull(1) 的 man page。


Git 也可以使用类似 CVS 的工作方式,使用一个中心仓库,所有用户将改动推送到仓库之中,相关内容可以查阅 git-push(1) 的手册页或 git for CVS users

浏览历史

Git 的历史是通过一系列相互关联的 commit 构成的。我们已经通过 git log 命令看到了这些提交的列表。注意,每个 git log 条目的第一行是那次提交的名称:

$ git log
commit c82a22c39cbc32576f64f5c6b3f24b99ea8149c7
Author: Junio C Hamano <junkio@cox.net>
Date: Tue May 16 17:18:22 2006 -0700

merge-base: Clarify the comments on post processing.

把这个名称用于 git show 命令,可以得到提交的详情。

$ git show c82a22c39cbc32576f64f5c6b3f24b99ea8149c7

不过还有其他办法来指代这次提交。你可以只使用名称的开始部分,只要它足够长,保证在所有提交中是惟一的就行了:

$ git show c82a22c39c   # the first few characters of the name are
# usually enough
$ git show HEAD # the tip of the current branch
$ git show experimental # the tip of the "experimental" branch

每一次提交通常都有一次提交作为 "parent" ,它是项目的前一个状态:

$ git show HEAD^  # to see the parent of HEAD
$ git show HEAD^^ # to see the grandparent of HEAD
$ git show HEAD~4 # to see the great-great grandparent of HEAD

Note that merge commits may have more than one parent:

要注意,合并提交可能会有多个 "parent":

$ git show HEAD^1 # show the first parent of HEAD (same as HEAD^)
$ git show HEAD^2 # show the second parent of HEAD

你还可以给你的提交一个名字; 命令

$ git-tag v2.5 1b2e1d63ff

让你可以使用 "v2.5" 来指代 1b2e1d63ff。如果你想把这个名字与他人共享 (比如标记一个发布版本),你应该建立一个 "tag" 对象,可能还需要签署它; 详情请查看 git-tag(1) 的 man page。


任何 git 命令都可以使用上述任何一种名字。比如:

$ git diff v2.5 HEAD     # compare the current HEAD to v2.5
$ git branch stable v2.5 # start a new branch named "stable" based
# at v2.5
$ git reset --hard HEAD^ # reset your current branch and working
# directory to its state at HEAD^

小心使用上述最后一个命令: 这将丢失工作目录中的所有改动,他还会清除本分支内随后的所有提交。如果这个分支是包含这些提交的惟一分支,它们将永远地丢失了。此外,不要对一个公众可见的、有其他开发者从中 pull 内容的分支使用 "git reset" 命令,这将导致一些不必要的合并来清除其他开发者的历史信息。如果你需要取消已经推送的改动,可以使用 git-revert(1) 命令。


git grep 可以在项目的所有版本历史中寻找字符串,而如下命令

$ git grep "hello" v2.5

会在版本 v2.5 中寻找所有 "hello" 的踪迹。


如果你不提供 commit 名称,git grep 会在你当前的目录中搜索所有由 git 管理的文件。于是,如下命令

$ git grep "hello"

是搜索 git 跟踪的所有文件的便捷的方式。


很多 git 命令可以处理一组提交,可以通过多种方式来指定版本。这里是一些 git log 的例子:

$ git log v2.5..v2.6            # commits between v2.5 and v2.6
$ git log v2.5.. # commits since v2.5
$ git log --since="2 weeks ago" # commits from the last 2 weeks
$ git log v2.5.. Makefile # commits since v2.5 which modify
# Makefile

你给出的范围的起始值不一定比终止值更老,比如,"stable-release" 分支可能会在 "master" 分支之后相当长一段时间才会引入同一个提交内容,这样

$ git log stable..experimental

将会列出 expermental 分支之中已经有的,而 stable 分支却还没有的提交,而命令

$ git log experimental..stable

将会列出 stable 中已有、但 experimental 却没有的提交。


"git log" 命令有一个弱点:必须将所有提交在一个列表中呈现出来。当项目历史中有多个不同开发分支并最终合并到一起时,"git log" 中呈现出来的顺序可能没什么意义。


大部分有大量开发者的项目 (比如 linux kernel 或 git 本身) 都经常合并分支,gitk 可以更好地将这些合并变化展示出来。比如,

$ gitk --since="2 weeks ago" drivers/

这个命令允许你浏览过去两个星期中在 "drivers" 目录之中的任意提交。(注意: 你可以按住 ctrl 键然后用 "-" 和 "+" 来调整 gitk 的字体大小。)


最后,大部分命令可以带有文件名,这可以用于指定某次提交中的某个文件,从而指定某个文件的某个版本:

$ git diff v2.5:Makefile HEAD:Makefile.in

你还可以用 "git show" 命令去查看任意文件的任意版本:

$ git show v2.5:Makefile

下一步学习

这个教程应该足够你的项目进行基本的项目发布版本管理。不过,要完全深入地理解 git 的强大功能可能还需要理解两个简单的概念:

  • 对象数据库是个相当阳春的系统,用于存储你的项目的历史,包括文件、目录以及各次提交。
  • 索引文件是目录树的状态的缓存,用于建立提交、取出工作目录并保存一次合并中包含的不同的目录树。

本教程的第二部分 解释了对象数据库、索引文件以及一些其他你在使用 git 中所需要了解的零七八碎的概念。


如果你不想在这条路上继续下去,还有一些细枝末节的东西可能十分有趣:

  • git-format-patch(1), git-am(1): 用于将一系列 git 提交转化成 email 发送的补丁或反之,对于 linux 内核这样的高度依赖于邮件发送的补丁的项目来说十分有用。

  • git-bisect(1): 当你的项目发生倒退的时候,一个跟踪发现错误的方法就是通过历史发现那个罪魁祸首的提交。Git bisect 可以帮助你进行二分查找发现那个提交。它能在一个有很多分支合并的具有复杂非线性历史的项目中十分灵巧地进行接近最优的搜索。
  • Everyday GIT with 20 Commands Or So

  • git for CVS users.


原文最后更新于: 11-Jun-2007 01:21:27 UTC

Sunday, June 03, 2007

智力竞赛节目的致命伤

从北京电视台的"星星擂台","SK 状元榜", 到中央台的"三星智力快车", 中学生知识竞赛类的节目我断断续续地看了十多年了, 一直以来以博古通今自居的我(其实也都是半瓶子醋, 远没到通的地步), 总觉得答题中学生的水平比不上中学时代的我, 不过, 在我看来, 这些节目的致命伤不在学生的水平, 而在编导和主持人的水平.

遥想当年, 星星擂台主持人旭东的水平似乎还是不错的, 至少在我的水平之内没看出什么问题来, 不过另两个节目却难以恭维. 那年看"三星智力快车", 一个同学把"忽必烈"答成"成吉思汗"了, 主持人在判断正误之余, 卖弄了一回"知识", 居然告诉该同学, 两者是父子关系, 从此, 我再也不看这个节目了...... 通过这件事, 我有一个明确的认识, 主持人不知道就不应该多说

不过今天, 我发现, 主持人需要控制的不只是自己, 今天一边早点一边看"SK 状元榜", 一道题要从三个图片中区分出"歼-10" 战斗机, 一位回答正确的同学解释的时候漏洞百出, 称 Su-27 为 Su-35, 称 J-10 的鸭翼为前掠翼, 主持人居然还称赞他太专业了.......

真不知道这样录制的节目怎么最终还播出了...

Monday, May 28, 2007

翻了翻《战争论》

周末在家的时候睡觉之前翻了翻多年前购入的卡尔. 冯. 克劳塞维茨的《战争论》 ,感觉和《孙子兵法》比强很多阿,呵呵,对战争本身分析得很深入,典型的西方的分析方式的思维。

Friday, May 25, 2007

我的致谢

感谢赋予我生命的父母;
感谢赐予我生活的世界;
感谢赠予我帮助的朋友;
感谢给予我爱情的女友。
当人生失意的时候,你会明白什么是值得珍惜的,什么是不得不放弃的,什么是应该铭记的;而当人生得意的时候,你会学到的只有一个词—— “感谢”。

当我结束五年硕士与博士研究生生涯的时候,我必须要感谢我的师长们,不论是给我高屋建瓴的言传身教张平老师,还是约束、关心我们一言一行的刘宝玲老师,更有亦师亦友的纪阳老师、唐晓晟老师、李亦农老师、冯智勇老师,你们或激昂、或平易、或严谨、或理性的言行无时无刻不在引导我前进。我也会感谢无线新技术研究所给我这片成长的土壤、这个挥洒的舞台,我从这里获得了很多、赢得了很多,也留下了一份永久的美好印象。

我当然还要感谢曾经的师兄师姐们,张新的领导风范、柴慧娟的古道热肠、王黎敏的热忱执着都在影响着我,永远也不会忘记他们所给我的教诲。还有同一级的挚友们:小白、大黄、小毕、老战、阿呆、罗总、胡峥……永远怀念我们一同拼搏的日子。

还不能忘记给我快乐、给我支持的师弟师妹们,也祝福你们。

当这份论文完成的时候,我不能不感谢帮助我完成整篇论文的那些开源软件的作者和贡献者们,他们包括但不限于:
  • Linus Torvalds, FSF 和其他无数贡献者开发的GNU/Linux 操作系统,是我完成整个论文的基础平台;
  • D. Knuth 教授和Leslie Lamport 的杰出发明:TEX 和LATEX 这是我所使用的排版系统,论文完成后,作者为论文所开发的模板将会留下来供其他人使用,这个模板还参考并部分使用了清华的王磊设计的清华大学博士论文模板的内容;
  • Werner Lamberg 的CJK是论文中文排版的核心支持;
  • 张林波老师的CCT 中文TEX 排版支持中的很多工具都被用在论文档中了,还要感谢oseen的打包工作;
  • NS-2 的开发者和贡献者们,以John W. Eaton 为首的Octave 的开发者,还有Maxima 的开发者们,他们提供的工具分别是论文中进行离散事件仿真、数值分析和符号运算所使用的工具。
  • Thomas Williams, Colin Kelley 和其他很多贡献者开发的Gnuplot,所有曲线、直方图、三位曲面图的绘制都由Gnuplot 完成,而精美的状态图、示意图大多由John Hobby 开发的Metapost 完成,此外其他的图片制造工具还有Dia, LATEX msc宏包等。
  • Bram Moolenaar等开发的VIM是本论文所用的编辑器,还有它的扩展LATEXsuit
在表达谢意的同时,我骄傲地向您,也向其他阅读者宣布:本论文的撰写过程中所使用的工具全部为开源软件或其它合法取得的软件,不仅内容是原创性的,而且完成过程也是合法、尊重著作权的。(注:本文所用的字体分别为文鼎免费字体和付费购买的方正字体。)

最后,我更要感谢这位伴我度过三年博士生涯的女孩—— 王海娟,她即将成为我的新娘,三年之中我们一起走过,不论是一同徜徉于校园还是天各一方、远隔重洋,我们的心间总有剪不断的红线。患难见真情,当我受尽挫折、接近放弃的时候,是她的坚定和果敢感染了我,让我义无反顾地向前冲,去挑战自我的顽疾,去战胜人性的弱点,她不仅是我疼爱的对象,更是我一同战斗一生的坚定伙伴。