https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html
Java SE平台的一个优点是它免除了开发人员的内存分配,垃圾回收的复杂工作,使开发人员专注与业务开发。 但是,当垃圾收集造成应用的性能瓶颈时,就需要开发人员了解这些垃圾收集机制的实现。垃圾收集器通过假设应用程序对对象的使用方式来设置垃圾收集参数, 垃圾收集器对应用程序使用对象的方式做出假设,这些可以反映在可调参数中,在性能调优中,可以调整这些参数。
当一个对象对正在运行的程序不可以任何方式可达时,会被标记为可收集(垃圾对象)。最简单的垃圾收集算法就是遍历所有的可达对象,任何没有被遍历到的对象即为垃圾对象。这种收集算法花费的时间和存活对象的个数成正比,这对于维护大量实时数据的大型应用程序来说是不可行的。
虚拟机合并了一系列垃圾收集算法,这些算法可以与分代收集(generational collection)方法结合使用。初级垃圾收集器遍历堆中的每个对象,分代收集器利用了几个大多数程序中的经验,来减少回收垃圾对象的工作量。最重要的经验就是弱世代假设(weak generational hypothesis),即大部分对象生存周期都很短。
有的对象确实生存周期比较长,所以在生命周期分布图中,分布曲线向右一直有延伸。例如,通常在初始化时分配一些对象,这些对象一直存在直到进程退出。在对象一直存活与朝生暮死之间,存在一些对象,在中间计算过程中一直存活。在图上即为右边凸起的一些尖点。 有的应用有完全不同的分布图形,但是绝大部分应用程序都拥有相似的分布图。高效的垃圾收集,就是建立在大部分对象“朝生暮死”的基础上的。
为了优化这种场景,内存是分代进行管理的(内存池持有不同年纪的对象)。在某代的空间要被用完时,这一代就会进行垃圾收集。大部分对象都被分配在一个专门为新生对象准备的对象池(新生代)中,并且大部分对象会在新生代被回收,当新生代要被用完时,会引起一次minor gc,只回收新生代的垃圾对象;其他代的垃圾对象不会被回收。基于弱世代假设(weak generational hypothesis),新生代的对象生存周期很短,新生代大部分对象都可以被回收的理论,minor gc 可以被优化。这些gc的成本首先与存活对象的的个数成正比;新生代一般充满垃圾对象,这些对象可以被快速回收。通常,在新生代经过gc存活下来的对象,一部分会在minor gc时被转移到老年代(tenured generation) 。最终,老年代的空间快用完,必须进行老年代垃圾收集是,会触发一次 major gc,在major gc期间,整个java堆都会被进行gc。因为涉及到大量对象的回收,major gc 通常比minor gc持续的时间长很多。
如在Ergonomics章节中介绍的内容,为了给不同种类的应用提供更好的性能,Ergonomics会动态选择垃圾收集器。
- 串行垃圾收集器(serial garbage collector)是为了小数据量的应用程序设计的,在小型应用中会被设置为默认的垃圾收集器。
- 并行垃圾收集器(parallel or throughput garbage collector) 是为了大中型数据量的应用程序设计的。
- Ergonomics选择的堆大小参数,以及其他自适应的策略参数,可以为服务应用程序提供更好的性能表现。这些参数在大部分服务应用中运行良好,但是也存在例外情况,即本文中说明的需要自行调参的情况。
注意:如果垃圾收集成为一个应用程序的性能瓶颈,则需要开发人员自定义堆大小,以及各个代的大小。检查垃圾收集的监控数据,并且探索各个垃圾收集参数对你程序性能的影响。
图 3-2 默认分代管理,
在初始化时,除非需要,实际上堆大小是被虚拟分配保留的,并没有实际分配物理内存。
为对象内存分配的空间(可能是虚拟分配),会被分为新生代和老年代。
新生代由eden区和两块survivor区组成的。大部分对象会初始化在eden区。在虚拟机运行过程中,会保证有一个survivor区始终是完全空余未被使用的,在新生代进行垃圾回收时,该空闲eden区将成为保存存活对象的区域,另一块eden区就会变为空闲状态,成为下一次垃圾回收时保存存活对象对区域。存活的对象会以这种方式在survivor区中来回被复制,直到对象足够老时,升级进入老年代。
性能指标
一般来说,主要由两个指标来评价GC的性能:
- 吞吐量
吞吐量是指 在长期运行后,花费在垃圾收集时间之外的时间所占的百分比。吞吐量包含了内存分配所花费的时间(但是通常不需要为内存分配来进行调优)。 - 暂停时间
暂停时间是指应用程序因为垃圾收集而对外界进行响应的时间。
用户通常对垃圾收集有不同的要求。如一些web服务,性能考量主要在于吞吐量,一些垃圾收集的停顿时间可能会被用户当作网络延迟而忽略。然而一些图形交互客户端程序,即使很短时间的停顿可能对用户体验有很糟糕的影响。
有些用户对其他指标比较敏感。
- 占用空间是指以内存页与缓存来衡量的一些列工作中的进程集合。当应用程序运行在有限的物理内存,或者和其他进程一起运行时,占用空间可以表明引用程序的可扩展性。
- 及时性是指当一个对象被标记为死亡,到该对象正式被回收,所占有的内存正式被释放的时间差。在分布式系统中,这是一个重要的性能指标,如RMI。
总的来说,选择新生代,老年代的大小,就是对以上性能指标的权衡过程。譬如,很大的年轻带,可能会提升吞吐量,但是会降低即时性,足迹大小,以及暂停时间。新生代暂停时间可以通过降低新生代的大小来实现,代价时降低吞吐量。
注意,每一代的大小,不影响其他带的垃圾收集频率以及暂停时间。
对于选择每一代的大小,没有所谓正确的选择,最佳选择是取决于应用程序使用内存的方式,以及用户的需求做。因此,虚拟机中关于gc的的设置并不总是最佳的,而且可能被“调整分代大小”这一节中的命令参数覆盖。
性能测量
吞吐量和占用空间最好使用应用程序特定的标准来衡量,譬如,一个web服务的吞吐量可能是使用一个客户端负载测试器测试的,占用空间是根据服务器上根据操作系统的具体命令来判断衡量的。然而,垃圾收集的暂停时间可以直接由虚拟机本身的诊断输出功能来衡量。
使用虚拟机命令行 -verbose:gc 会在每次垃圾收集时打印出堆以及垃圾收集的相关信息。譬如,以下是某大型服务应用的输出信息:
[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]
这些输出信息表示,虚拟机经过了两次minor gc后,进行了一次full gc。箭头前后的数字(如第一次的 325407K->83000K)分别代表了垃圾回收前后,存活对象的总大小。经过一次minor gc后,剩余的对象集中包含了一些无法被回收,但是实际已经不再存货的对象。这些对象可能包含在老年代,或者被老年代中的对象引用。
接下来的一个数字(譬如第一次的776768K) ,是堆的已申请大小:即无需再向操作系统申请更多内存,现在即可用于为分配对象使用的堆大小。请注意,这个大小包含一个survival区的大小,除了在垃圾收集时,只有一个survivor 区会被用来分配对象。
最后一个数字(譬如第一次的 0.2300771 secs)表示此次垃圾收集花费的时间,此次垃圾收集约为1/4秒。
full GC中的参数也是类似含义。
注意: verbose:gc生成的输出格式在将来的版本中可能会发生变化。
命令行选项-XX:+ PrintGCDetails会增加打印垃圾收集的其他信息,以下为此命令行打印其他信息的示例:
[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]
以上信息代表了一个minoc gc的过程,回收了约98%的新生代空间(DefNew: 64575K->959K(64576K)),花费时间 0.0457646 s (约45 ms).整个堆的大小约减少了 51%( (196016K->133633K(261184K))),相比新生代,收集成本有一些轻微的额外增多,花费时间0.0459067 secs.
注意: -XX:+PrintGCDetails生成的输出格式在将来的版本中可能会发生变化。
命令行选项-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]
以上垃圾收集约开始于开始执行应用程序的111s时,minor gc约开始于同一时间。此外,以上信息还展示了老年代中的major gc的信息。在major gc中,老年代使用空间约回收10%(18154K->2311K(24576K)),花费 0.1290354 secs(约130ms)
T