02_jvm性能调优_垃圾收集器的实现

Java SE 平台的一个优势是,它使开发人员免去了内存分配和垃圾收集的复杂性。

然而,当垃圾收集成为主要的瓶颈时,了解实现的一些方面是很有用的。垃圾收集器对应用程序使用对象的方式做出假设,这些假设反映在可调参数中,这些参数可以在不牺牲抽象能力的情况下进行调整以提高性能。

分代垃圾收集(Generational Garbage Collection)

当一个对象无法从运行中程序的任何其他存活对象引用到时,它被视为垃圾,其内存可以被虚拟机重新利用。

理论上,最直接的垃圾回收算法是每次运行时迭代遍历每个可达对象。剩余的对象被认为是垃圾。这种方法所需的时间与存活对象的数量成正比,对于维护大量活动数据的大型应用程序来说是不可接受的。

Java HotSpot虚拟机整合了多种不同的垃圾回收算法,使用一种称为代际收集(Generational Collection)的技术。而传统的垃圾回收会在每次检查堆中的所有存活对象,代际收集利用了大多数应用程序的经验观察属性来最小化重新获取未使用(垃圾)对象所需的工作量。其中最重要的观察属性之一是弱代际假设(Weak Generational Hypothesis),该假设述说大多数对象仅生存很短一段时间。

在图3-1中,蓝色区域代表对象生命周期的典型分布。X轴显示以分配的字节数来衡量的对象生命周期,Y轴上的字节数表示具有相应生命周期的对象的总字节数。左侧的尖峰代表可以在分配后不久被回收(也就是"死亡")的对象。例如,迭代器对象通常只在单个循环的持续时间内存活。

一些对象的寿命更长,因此分布会延伸到右侧。例如,通常在初始化时分配的一些对象会存活直到虚拟机退出。在这两个极端之间的是一些在某些中间计算过程中存活的对象,可以看到它们位于初始峰值右侧的 lump 区域。一些应用程序具有非常不同的分布形状,但令人惊讶的是有大量的应用程序具有这种一般形状。通过专注于绝大多数对象"英年早逝"的事实,可以实现高效的收集。

分代(Generations)

为了优化垃圾回收,内存被管理在不同的代(持有不同年龄对象的内存池)中。当某一代填满时,就对该代进行垃圾回收。

根据描述,大部分对象都是分配在专门用于年轻对象的内存池(即年轻代)中,而大多数对象也在这里被回收。当年轻代填满时,会触发一次小型垃圾回收(Minor GC),在该回收中只有年轻代被清理;其他代中的垃圾不会被回收。这种回收的成本,一般来说,与被收集的存活对象数量成正比;因此,当年轻代充满已经死亡对象时,其清理速度非常快。通常,在每次小型回收期间,部分幸存的对象会从年轻代移动到老年代。最终,老年代填满时必须进行全面回收,即进行一次主要回收(Major GC),在这个过程中整个堆都会被清理。由于涉及到的对象数量显著更多,主要回收通常比小型回收持续时间更长。Figure 3-2展示了串行垃圾回收器中各代的默认排列方式。

对于串行垃圾回收器,请参照相关文档或图表以获取更多详细信息和特定排列。同时也可以考虑使用不同类型的垃圾回收器以优化性能和资源利用。

在Java HotSpot虚拟机启动时,会在地址空间中预留整个Java堆,但并不会为其分配物理内存,除非需要。覆盖Java堆的整个地址空间在逻辑上被划分为年轻代和老年代。用于对象存储的完整地址空间可以被划分为年轻代和老年代。

这种内存管理模式可以灵活地根据具体的内存需求来动态分配物理内存,以实现更高效的内存利用和管理。这种预留和按需分配的方式有助于避免不必要的内存浪费,并能够更好地满足应用程序对内存资源的需求。

根据描述,年轻代由Eden区和两个幸存者空间组成。大部分对象最初都是分配在Eden区。在任何时候,有一个幸存者空间是空的,用作在垃圾回收期间将Eden区和另一个幸存者空间中的存活对象拷贝到该空间中;回收后,Eden区和源幸存者空间都是空的。在下一次垃圾回收时,两个幸存者空间的作用会交换。最近被填满的那个空间成为需要拷贝到另一个幸存者空间中的存活对象的源空间。以这种方式在幸存者空间之间拷贝对象,直到它们被拷贝了一定次数或剩余空间不足。这些对象会被拷贝到老年代。这个过程也称为"aging"(老化)。

性能考虑( Performance Considerations**)**

在分代垃圾收集中,需要考虑的主要性能因素是吞吐量和暂停时间。

  • 吞吐量(Throughput):吞吐量是运行用户代码的时间占总运行时间的比例。总运行时间包括程序的运行时间和内存回收的时间。高吞吐量的应用程序可以容忍较高的暂停时间,因为它们有更长的时间基准,快速响应不是首要考虑的因素。
  • 暂停时间(Pause Time):暂停时间是执行垃圾收集时,程序的工作线程被暂停的时间。对于需要快速响应的应用程序,如Web服务器或实时系统,减少暂停时间是非常重要的。

不同用户对于垃圾回收有不同的需求。例如,在Web服务器中,一些人认为吞吐量是一个合适的度量标准,因为垃圾回收期间的暂停可能是可以被容忍或者简单地被网络延迟所掩盖。然而,在一个交互式图形程序中,即使是短暂的暂停也可能会对用户体验产生负面影响。

一些用户可能对其他因素更为敏感。内存占用是进程的工作集,以页面和缓存行为单位来衡量。在物理内存有限或进程较多的系统中,内存占用可能决定了可伸缩性。及时性是指对象变为死态到内存可用之间的时间间隔,在包括远程方法调用(RMI)在内的分布式系统中是一个重要考虑因素。

通常情况下,选择特定代的大小是在这些考虑因素之间进行权衡。举例来说,一个非常大的年轻代可能会最大化吞吐量,但却是以牺牲内存占用、及时性和暂停时间为代价的。小年轻代可以最小化年轻代的暂停时间,但会牺牲吞吐量。一个代的大小不会影响另一个代的回收频率和暂停时间。

选择一个代的大小并没有一个标准的方法。最佳选择取决于应用程序如何使用内存以及用户的需求。因此,虚拟机对垃圾回收器的选择并不总是最优的,可以使用命令行选项来覆盖。请参阅影响垃圾回收性能的因素。

吞吐量和内存占用测量( Throughput and Footprint Measurement**)**

吞吐量和内存占用最好使用特定于应用程序的指标来衡量。

例如,可以使用客户端负载生成器测试Web服务器的吞吐量。然而,由于垃圾回收而引起的暂停可以通过检查虚拟机本身的诊断输出来轻松估算。命令行选项-verbose:gc会在每次垃圾回收时打印有关堆和垃圾回收的信息。以下是一个示例:

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

关于这个例子的输出,显示了两次年轻代的垃圾回收,然后是应用程序调用System.gc()触发的一次完全垃圾回收。每行开头都有一个时间戳,表示从应用程序启动开始的时间。接下来是关于此行的日志级别(info)和标签(gc)的信息。然后是一个垃圾回收标识号,这里有三个GC,分别是36、37和38。然后是垃圾回收的类型和触发GC的原因。接着记录了一些关于内存消耗的信息。该日志使用格式"在GC之前使用" -> "在GC之后使用" ("堆大小")。

在示例中第一行是239M->57M(307M),意味着在进行垃圾回收之前使用了239MB内存,并且垃圾回收清理了大部分内存,但仍有57MB幸存下来。堆大小为307MB。需要注意的是,在这个示例中,完全垃圾回收将堆大小从307MB缩小到104MB。内存使用信息之后,还记录了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

注意:-Xlog: gc * 产生的输出格式可能会在未来的版本中发生变化。

相关推荐
喵叔哟6 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生12 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow26 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
不是二师兄的八戒35 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
小牛itbull36 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i44 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
爱编程的小生1 小时前
Easyexcel(2-文件读取)
java·excel
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++