3.垃圾回收器的机制

Java SE平台的一个优势是它使得开发人员免受复杂的内存分配和垃圾回收的影响。

但是,当垃圾回收是主要瓶颈时,了解垃圾回收的相关机制很是有用。垃圾回收器通过对应用程序如何使用对象进行模拟,进而通过可调参数提高应用程序性能,而不用牺牲java强大的抽象能力。

分代的垃圾回收机制

垃圾对象的定义:当一个对象不能从运行程序中的任何其他活动对象的任何引用到达它时,这个对象认为是垃圾。虚拟机可以重新使用垃圾对象占用的内存。理论上最简单的垃圾回收算法是每次运行时都会遍历每个可到达的对象,任何剩余不可到达的对象都被认为是垃圾。这种方法需要的时间与活动对象的数量成正比,这对于大量需要保持大量活动回想的应用程序而言是不可行的。

Java HotSpot VM包含许多不同的垃圾回收算法,这些算法都是基于分代回收技术。 虽然垃圾回收每次都会检查堆中的每个活动对象,但是分代收集会利用大多数应用程序的一些特别特性来最小化回收内存垃圾对象所需的事件。 这些特性中最重要的是弱年代假设:大多数对象在内存中存活的时间都很短。

分代

为了解决垃圾回收执行时间长的问题,Java HotSpot VM使用分代管理内存的方法,即根据内存对象不同的生存周期放入不同的代中,每个代内存满时才进行内存回收操作。

主要的新对象被分配在专用于年轻对象的内存池中(年轻代),其中绝大部分对象的生存期都都短,都会在这里被回收。 当年轻代满时,它会执行一次minorGC ,其只收集年轻一代中垃圾对象,其他代中垃圾对象不做回收。 这种垃圾回收的成本与年轻代中存货对象的数目成正比,年轻代中垃圾对象回收的会非常快。 通常经过多次minorGC年轻一代的幸存对象会被移动到老年代 。 最终,老年代中的内存被占满触发执行一次MajorGC。MajorGC会回收老年代中的垃圾对象。因为老年代中的对象比年轻代多得多,因此MajorGC所需要的事件会比MinorGC大得多。 图3-2显示了串行垃圾回收器中代的默认排列方式:
avatar
图3-2 Serial Collector中年轻代和老年代的结构

在启动时,Java HotSpot VM会在地址空间中保留整个Java堆所需的地址公开,但只要在正在需要使用时才会分配物理内存。 Java堆的整个地址空间在逻辑上分为年轻代和旧代。

年轻一代由Eden区和两个Survivor区组成。 大多数对象最初都是在Eden中分配的。两个Survivor区中的一个在任何时候都是空的,为的是在Eden区和非空的Surviror区做垃圾回收时,将未回收掉的对象转移到空Survivor区中。 在垃圾回收之后, Eden和非空Survivor区都变了空区,原来空Survivor变成了非空Survivor。下一次垃圾回收已久会执行上述操作,将Eden和非空Survivor区中不可回收对象转移到空Survivor区中,以此往复,直到一定回收次数后或没有足够的空间时,已久存货的对象被复制到老年代,此过程也称为老化。

性能考虑

垃圾回收考虑的主要指标是吞吐量和延迟。

吞吐量是在未在垃圾回收的时间与垃圾回收总时间的百分比。 吞吐量包括花在分配对象上的时间。

延迟是应用程序的及时响应性。 垃圾回收暂停会影响应用程序的响应能力。

用户对垃圾回收有不同的要求。 例如,有些人认为Web服务器的正确度量是吞吐量,因为垃圾回收期间的暂停可能是容忍的,因为短暂延迟会被网络的延迟所掩盖。 但是,在交互式图形程序中,即使是短暂的暂停也会对用户体验产生负面影响。

一些用户对其他因素比较敏感,比如Footprint和Promtness。 Footprint指的是一个进程的工作集,以占用页面和缓存空间来衡量。 在有限物理内存或许多进程的系统上,占用空间可能会影响系统的可伸缩性。 Promptness指对象变为死亡和其占用的内存变为可用之间的时间间隔,这是分布式系统(包括远程方法调用(RMI))的一个重要考虑因素。

一般来说,选择年轻代和老年的大小是各个影响因素之间的权衡。 例如,一个非常大的年轻一代可能会使吞吐量最大化,但是这会牺牲空间,及时性和提高暂停时间。 年轻一代的停顿可以通过调小年轻代的大小来达到,但这同时会牺牲(减少)系统吞吐量。 某代的大小不会影响另一代的收集频率和暂停时间。

选择某代的大小没有一个正确的方法。 最佳选择取决于应用程序使用内存的方式以及用户要求。 虚拟机对垃圾回收器的默认配置并不总是最佳的,因此可以通过可配置选项来覆盖默认配置; 请参阅下一章: 影响垃圾回收性能的因素 。

吞吐量和当前使用堆大小的测量

吞吐量和占用空间最好使用特定于应用程序的工具来测量。

例如,可以使用客户端负载生成器来测试Web服务器的吞吐量,也可以使用pmap命令在Solaris操作系统上测量服务器的占用空间。 但是,通过检查虚拟机本身的监控输出,可以轻松地估计垃圾回收造成的暂停。

在java程序运行时加入选项-verbose:gc可以在程序运行时打印相关的堆和垃圾回收的信息。 例如:

  1. [15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms
  2. [16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms
  3. [16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms

输出显示了两个Yong GC,和一个通过调用System.gc()启动的Full GC。 每一行行以时间戳开头,表示从应用程序启动开始计时到本次GC结束后的当前的时间。 接下来是关于此行的日志级别(info)和标签(gc)。 随后是GC识别号,分别为36,37和38.然后记录GC的类型和什么导致了本次GC。 随后记录了有关内存消耗的一些信息,可以理解为:GC前使用的堆大小->GC后的堆大小,括号中表示总的堆大小。随后的时间表示(本次GC开始的时间,本次GC完成时的时间)和本次GC共消耗的毫秒。

在示例的第一行中,这是239M-> 57M(307M),这意味着在GC之前使用了239 MB,GC清除了大部分内存,GC后只有57MB存活。 堆大小为307 MB。 注意在这个例子中,完整的GC将堆从307 MB缩小到104 MB。

-verbose:gc命令是-Xlog:gc的别名。 -Xlog是用于登录HotSpot JVM的常规日志记录配置选项。 它是一个基于标签的系统,其中gc是标签之一。 要获得有关GC正在执行的更多信息,可以配置日志记录以打印任何具有gc标记和任何其他标记的消息。 命令行选项是-Xlog:gc* 。

以下是使用-Xlog:gc记录的一个-Xlog:gc

  1. [10.178s][info][gc,start ] GC(36) Pause Young (G1 Evacuation Pause)
  2. [10.178s][info][gc,task ] GC(36) Using 28 workers of 28 for evacuation
  3. [10.191s][info][gc,phases ] GC(36) Pre Evacuate Collection Set: 0.0ms
  4. [10.191s][info][gc,phases ] GC(36) Evacuate Collection Set: 6.9ms
  5. [10.191s][info][gc,phases ] GC(36) Post Evacuate Collection Set: 5.9ms
  6. [10.191s][info][gc,phases ] GC(36) Other: 0.2ms
  7. [10.191s][info][gc,heap ] GC(36) Eden regions: 286->0(276)
  8. [10.191s][info][gc,heap ] GC(36) Survivor regions: 15->26(38)
  9. [10.191s][info][gc,heap ] GC(36) Old regions: 88->88
  10. [10.191s][info][gc,heap ] GC(36) Humongous regions: 3->1
  11. [10.191s][info][gc,metaspace ] GC(36) Metaspace: 8152K->8152K(1056768K)
  12. [10.191s][info][gc ] GC(36) Pause Young (G1 Evacuation Pause) 391M->114M(508M) 13.075ms
  13. [10.191s][info][gc,cpu ] GC(36) User=0.20s Sys=0.00s Real=0.01s