平衡GC线程上的工作
在server gc中,每个GC线程都将并行地在其堆上工作(这是一个简单化的视图,不一定对所有阶段都适用,但在较高的层次上,这正是并行GC的概念)。因此,仅此一项就意味着工作已经在GC线程之间被分割了。但是,由于某些阶段的GC工作只能在所有线程完成其最后一个阶段之后才能继续(例如,在所有GC线程都完成标记阶段之前,我们不能让任何GC线程从计划阶段开始,这样我们就不会错过应该标记的对象),所以我们希望每个线程上的GC工作量尽可能平衡总的暂停时间可以更短,否则,如果一个线程花费很长时间来完成这样的阶段,其他线程将等待不做任何事情。为了使工作更加平衡,我们做了很多事情。我们将继续做这样的工作来平衡更多。
平衡分配
平衡收集工作的一种方法是平衡分配。当然,即使每个堆具有完全相同的分配量,收集工作的量仍然可能非常不同,这取决于生存期。但这确实有帮助。因此,我们在GC结束时均衡分配预算,以便每个堆获得相同的分配预算。这并不意味着每个堆自然会获得相同的分配量,但它对每个堆在触发下一个GC之前可以执行的分配量设置了相同的上限。当然,分配线程的数量和每个分配线程所做的分配量取决于用户代码。我们试图在与运行分配线程的核心相关联的堆上进行分配,但是由于我们没有控制权,我们需要检查是否应该平衡到其他最不满的堆,并在适当的时候平衡它们。“在适当的时候”需要一些仔细的调优启发。目前,我们要考虑线程运行的核心,它运行的NUMA节点,它与其他堆相比还有多少分配预算,以及有多少分配线程在同一个内核上运行。我确实认为这有点不必要的复杂,所以我们正在做更多的工作,看看我们是否可以简单地做到这一点。
如果我们使用GCHeapCount配置来指定比核心更少的堆,这意味着只有那么多GC线程,并且默认情况下它们只能在那么多的内核上运行。当然,用户线程可以在其余核心上自由运行,它们所做的分配将平衡到GC堆上。
如果我们使用GCHeapCount配置来指定比核心更少的堆,这意味着只有那么多GC线程,并且默认情况下它们只能在那么多的内核上运行。当然,用户线程可以在其余核心上自由运行,它们所做的分配将平衡到GC堆上。
平衡GC工作
在GC中进行的大多数当前平衡都集中在标记上,因为标记通常是花费最长时间的阶段。如果你要选择要平衡的任务,那么平衡最容易失衡的最长部分是更有意义的——平衡工作不是没有代价的。
标记使用一个标记堆栈,这使得它成为工作偷窃的自然目标。当一个GC线程完成了自己的标记后,它会四处查看其他线程的标记堆栈是否仍在忙,如果仍然忙,则窃取一个对象进行标记。由于我们实现了“部分标记”,这意味着如果一个对象包含许多引用,我们一次只将其中的一块推送到标记堆栈上,而不会溢出堆栈。这意味着堆栈上的条目可能不是直接的对象地址。窃取需要识别特定的序列,以确定是否应该搜索其他条目,或者读取该序列中正确的条目进行窃取。请注意,这只在完全封锁地面军事系统期间开启,因为在某些情况下偷窃确实会造成明显的成本。
性能工作主要由用户场景驱动。随着我们的框架越来越多地被高性能场景使用,我们一直在努力缩短暂停时间。有人问过并行压缩GCs,是的,我们的路线图上确实有。但这并不意味着我们将停止改进我们目前的GC。我们在查看客户数据时注意到的一点是,当我们进行短暂的GC时,标记老一代对象所指向的年轻一代对象通常需要花费最长的时间。最近,我们在5.0中实现了工作窃取,通过让每个GC线程每次都要处理老一代的一大块。它以原子方式增加块索引,因此如果另一个线程也在查看同一代,那么它将获取下一个尚未获取的块。复杂的是我们可能有多个片段,所以我们需要跟踪当前正在处理的片段(及其起始索引)。当一个线程刚刚到达一个已经被其他线程处理过的段时,它知道要前进超过这个段。每个块保证只由一个线程处理。由于这一保证,而且将指针重新定位到年轻的gen对象共享相同的代码路径,这意味着重新定位工作也以同样的方式得到了平衡。
我们也在阶段结束时做平衡工作,这样就可以平衡在同一阶段发生的早期工作中的不平衡。
还有其他类型的平衡,但这些是最主要的。STW-GCs可以平衡更多的工作。我们选择更多地关注标记阶段,因为这是最需要的。我们没有平衡并发工作,仅仅是因为当你同时运行时它更容易被原谅。很明显,平衡这一点也是有好处的,所以这是一个去做的问题。
标记使用一个标记堆栈,这使得它成为工作偷窃的自然目标。当一个GC线程完成了自己的标记后,它会四处查看其他线程的标记堆栈是否仍在忙,如果仍然忙,则窃取一个对象进行标记。由于我们实现了“部分标记”,这意味着如果一个对象包含许多引用,我们一次只将其中的一块推送到标记堆栈上,而不会溢出堆栈。这意味着堆栈上的条目可能不是直接的对象地址。窃取需要识别特定的序列,以确定是否应该搜索其他条目,或者读取该序列中正确的条目进行窃取。请注意,这只在完全封锁地面军事系统期间开启,因为在某些情况下偷窃确实会造成明显的成本。
性能工作主要由用户场景驱动。随着我们的框架越来越多地被高性能场景使用,我们一直在努力缩短暂停时间。有人问过并行压缩GCs,是的,我们的路线图上确实有。但这并不意味着我们将停止改进我们目前的GC。我们在查看客户数据时注意到的一点是,当我们进行短暂的GC时,标记老一代对象所指向的年轻一代对象通常需要花费最长的时间。最近,我们在5.0中实现了工作窃取,通过让每个GC线程每次都要处理老一代的一大块。它以原子方式增加块索引,因此如果另一个线程也在查看同一代,那么它将获取下一个尚未获取的块。复杂的是我们可能有多个片段,所以我们需要跟踪当前正在处理的片段(及其起始索引)。当一个线程刚刚到达一个已经被其他线程处理过的段时,它知道要前进超过这个段。每个块保证只由一个线程处理。由于这一保证,而且将指针重新定位到年轻的gen对象共享相同的代码路径,这意味着重新定位工作也以同样的方式得到了平衡。
我们也在阶段结束时做平衡工作,这样就可以平衡在同一阶段发生的早期工作中的不平衡。
还有其他类型的平衡,但这些是最主要的。STW-GCs可以平衡更多的工作。我们选择更多地关注标记阶段,因为这是最需要的。我们没有平衡并发工作,仅仅是因为当你同时运行时它更容易被原谅。很明显,平衡这一点也是有好处的,所以这是一个去做的问题。
未来的工作
继续努力使事情更加平衡。除了更多地平衡当前任务外,我们还需要改变堆的组织方式,使平衡更加自然(因此线程与堆的耦合不是那么紧密)。
为虫子生,为虫子死,为虫子奋斗一辈子