Java SE 平台的优势之一是它使开发人员免受内存分配和垃圾收集的复杂性影响。但是,当垃圾收集是主要瓶颈时,了解实现的某些方面很有用。垃圾收集器对应用程序使用对象的方式做出假设,这些假设反映在可调参数中,这些参数可以在不牺牲抽象能力的情况下进行调整以提高性能。

分代垃圾收集

一个对象被认为是垃圾,当它不能再从正在运行的程序中任何其他活动对象的任何引用中访问时,它的内存可以被 VM 重用。理论上的、最直接的垃圾收集算法每次运行时都会遍历每个可访问的对象。任何剩余的对象都被视为垃圾。这种方法花费的时间与活动对象的数量成正比,这对于维护大量活动数据的大型应用程序来说是望而却步的

Java HotSpot VM 结合了许多不同的垃圾收集算法,这些算法都使用称为分代收集的技术。虽然原始垃圾收集每次都会检查堆中的每个活动对象,但分代收集利用大多数应用程序的几个经验观察属性来最小化回收未使用(垃圾)对象所需的工作。这些观察到的属性中最重要的是弱世代假设,它指出大多数对象只能存活很短的时间

图 3-1 中的蓝色区域是对象生命周期的典型分布。x 轴显示以分配的字节数衡量的对象生命周期。y 轴上的字节数是具有相应生命周期的对象中的总字节数。左边的尖峰代表在分配后不久可以回收(换句话说,已经“死亡”)的对象。例如,iterator对象通常只在单个循环期间处于活动状态。

图 3-1 对象生命周期的典型分布
image.png

有些对象确实寿命更长,因此分布向右延伸。例如,通常有一些在初始化时分配的对象会一直存在到 VM 退出。在这两个极端之间是在一些中间计算期间存活的对象,这里被视为初始峰值右侧的凸起。一些应用程序具有非常不同的外观分布,但大量应用程序惊人的具有这种一般形状。通过关注大多数对象“早逝”这一事实,使高效收集成为可能。

分代

为了针对这种情况进行优化,内存是分代管理的(内存池包含不同年龄的对象)。当代填满时,垃圾收集在每一代中发生。绝大多数对象都分配在一个专用于年轻对象(年轻代)的池中,并且大多数对象都死在那里。当年轻代填满时,会导致只回收年轻代的minor collection,而其他世代的垃圾没有被回收。这种收集的成本首先与被收集的活动对象的数量成正比;一个充满死对象的年轻代会很快被收集起来。通常,在每个minor 收集期间,年轻代中幸存的对象的一部分被移动到年老代最终,老年代填满,必须收集,导致 major 收集,其中收集了整个堆major 收集的持续时间通常比 minor 收集 长得多,因为涉及的对象数量要多得多

图 3-2显示了串行垃圾收集器中的默认生成排列:
image.png

在启动时,Java HotSpot VM 将整个 Java 堆保留在地址空间中,但除非需要,否则不会为其分配任何物理内存。覆盖Java堆的整个地址空间在逻辑上分为年轻代和年老代。为对象内存保留的完整地址空间可以分为年轻代和年老代。年轻代由Eden空间和两个幸存者空间组成。大多数对象最初是在 eden 中分配的。一个survivor空间在任何时候都是空的,作为eden中存活对象的目的地,另一个survivor空间用于垃圾收集,Eden和源survivor空间为空。在接下来的垃圾回收中,交换了两个survivor空间的用途。最近填充的一个空间是复制到另一个幸存者空间的活动对象的来源。对象以这种方式在幸存者空间之间复制,直到它们被复制一定次数或没有足够的空间剩余。这些对象被复制到年老代中。这个过程也称为老化。

性能注意事项

垃圾收集的主要措施是吞吐量和延迟。吞吐量是在很长一段时间内未花费在垃圾收集上的总时间的百分比。吞吐量包括在分配上花费的时间(但通常不需要调整分配速度)。延迟是应用程序的响应能力。垃圾收集暂停会影响应用程序的响应能力。

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

一些用户对其他考虑因素很敏感。Footprint是进程的工作集,以页面和缓存行来衡量。在物理内存有限或有许多进程的系统上,占用空间可能决定了可扩展性。及时性是当一个对象从死亡到内存可用之间的时间,分布式系统,包括远程方法调用(RMI)的重要考虑因素。

通常,要为特定世代选择大小在这些考虑因素之间权衡。例如,一个非常大的年轻代可能会最大化吞吐量但这样做是以占用空间、及时性和暂停时间为代价的可以通过以牺牲吞吐量为代价使用小的年轻代来最小化年轻代的暂停。一代的大小不会影响另一代的收集频率和暂停时间。

没有一种正确的方法可以选择代的大小。最佳选择取决于应用程序使用内存的方式以及用户要求。因此,虚拟机对垃圾收集器的选择并不总是最佳的,可能会被命令行选项覆盖;请参阅影响垃圾收集性能的因素。

吞吐量和足迹测量

吞吐量和足迹最好使用特定于应用程序的指标来衡量。例如,可以使用客户端负载生成器测试 Web 服务器的吞吐量,而可以使用该pmap命令在 Solaris 操作系统上测量服务器的占用空间。但是,通过检查虚拟机本身的诊断输出可以轻松估计由于垃圾收集导致的暂停。

命令行选项-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

输出显示两个minor collection,然后是一个full GC,该 GC 由应用程序通过调用System.gc()发起. 这些行以一个时间戳开始,指示应用程序启动的时间。接下来是有关此行的日志级别 (info) 和标记 (gc) 的信息。后面跟着一个 GC 识别号。在这种情况下,有三个 GC,编号分别为 36、37 和 38。然后记录了 GC 的类型和说明 GC 的原因。在此之后,会记录一些有关内存消耗的信息。该日志使用格式“在 GC 之前使用”->“在 GC 之后使用”(“堆大小”)。

在示例的第一行中,这是 239M->57M(307M),这意味着在 GC 之前使用了 239 MB,并且 GC 清除了大部分内存,但 57 MB 幸存下来。堆大小为 307 MB。请注意,在此示例中,full GC 将堆从 307 MB 缩小到 104 MB。在内存使用信息之后,将记录 GC 的开始和结束时间以及持续时间(结束 - 开始)。

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

这是一个 G1 年轻集合的示例,记录为-Xlog:gc*:

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