实时(RT)应用程序开发通过对部分运行时行为施加时间限制,使其与通用应用程序开发区分开来。 此类限制通常放在应用程序的各个部分(例如中断处理程序)上,其中响应中断的代码必须在给定时间段内完成其工作。 当诸如心脏监护仪或国防系统之类的硬RT系统错过了这些最后期限时,就被认为是整个系统的灾难性故障。 在软RT系统中,错过最后期限可能会带来不利影响-例如GUI不能显示其监视的流的所有结果-但不会构成系统故障。
在Java应用程序中,Java虚拟机(JVM)负责优化运行时行为,管理对象堆以及与操作系统和硬件进行接口。 尽管在语言和平台之间的此管理层简化了软件开发,但它在程序中引入了一定量的开销。 GC是此类领域之一,通常会导致应用程序中的不确定性暂停。 暂停的频率和持续时间都是无法预测的,这使得Java语言传统上不适合RT应用程序开发。 一些基于Java实时规范(RTSJ)的现有解决方案使开发人员可以回避Java技术的不确定性方面,但要求他们更改现有的编程模型。
Metronome是确定性垃圾收集器,可为标准Java应用程序提供有限的低暂停时间和指定的应用程序利用率 。 减少的有限停顿时间是由于采用了渐进式的收集方法以及经过认真的工程决策(包括对VM的根本更改)而导致的。 利用率是指允许应用程序在特定时间范围内运行的时间百分比,其余时间专用于GC。 节拍器使用户可以指定应用程序接收的利用率级别。 与RTSJ结合使用时,Metronome使开发人员能够构建具有确定性的软件,具有短的暂停时间,并且在定时窗口非常小时可以无暂停。 本文介绍了传统GC用于RT应用程序的局限性,详细介绍了Metronome的方法,并提供了使用Metronome开发硬RT应用程序的工具和指南。
传统的GC实现使用停世界 (STW)方法恢复堆内存。 应用程序将一直运行,直到堆中的可用内存用完为止,这时GC停止所有应用程序代码,执行垃圾回收,然后让该应用程序继续运行。
图1说明了传统的STW暂停GC活动,其频率和持续时间通常都是无法预测的。 传统的GC是不确定的,因为恢复内存所需的工作量取决于应用程序使用的对象的总数和大小,这些对象之间的互连以及释放足够的堆内存以满足未来分配所需的工作水平。
通过检查GC的基本组成部分,您可以了解为什么GC时间不受限制和不可预测。 GC暂停通常包括两个不同的阶段: 标记阶段和清除阶段。 尽管许多实现和方法可以组合或修改这些阶段的含义,或者通过其他方式(例如,压缩以减少堆中的碎片)增强GC,或使某些阶段与正在运行的应用程序同时运行,但这两个概念是技术性的传统GC的基准。
标记阶段负责跟踪对应用程序可见的所有对象,并将它们标记为活动对象,以防止回收它们的存储。 此跟踪从根集开始, 根集由内部结构(例如线程堆栈和对对象的全局引用)组成。 然后,它遍历引用链,直到标记了根集中的所有(直接或间接)可访问对象。 在标记阶段结束时未标记的对象是应用程序( dead )无法访问的,因为从根集到通过任何系列引用都没有找到它们的路径。 标记阶段的长度是无法预测的,因为无法预测应用程序在任何特定时间的活动对象的数量以及遍历所有引用以查找系统中所有活动对象的成本。 行为一致的系统中的预言家可以根据以前的计时特性来预测时间需求,但是这些预测的准确性将成为不确定性的另一个来源。
清除阶段负责在标记完成后检查堆,并将死对象的存储回收到空闲存储中以进行堆,从而使该存储可用于分配。 与标记阶段一样,无法完全预测将死对象扫回到空闲内存池中的成本。 尽管可以从标记阶段得出系统中活动对象的数量和大小,但是它们在堆中的位置以及它们对空闲内存池的适用性都可能需要不可预测的分析水平。
RT应用程序必须能够在确定的时间间隔内响应现实世界的刺激。 传统的GC无法满足此要求,因为应用程序必须暂停才能回收任何未使用的内存。 复垦所花费的时间是不受限制的,并且可能会有所波动。 此外,GC中断应用程序的时间在传统上是不可预测的。 暂停应用程序的时间称为暂停时间,因为暂停了GC回收可用空间的应用程序进度。 RT应用程序需要低的暂停时间,因为它们通常代表应用程序响应时间的上限。
Metronome的方法是将消耗GC周期的时间分成一系列称为Quanta的增量。 为此,每个阶段都设计为通过一系列分立的步骤来完成其全部工作,从而使收集器能够:
该顺序与传统模型相反,在传统模型中,应用程序在无法预测的点停止运行,GC在无限制的时间内运行完成,然后GC静默以使应用程序恢复。
尽管将STW GC周期分成短暂的暂停有助于减少GC的影响,但这对于RT应用程序来说还不够。 为了使RT申请能够按时完成任务,必须在任何给定时间段内充分分配时间; 否则,将违反要求,并且应用程序将失败。 例如,假设GC暂停的时间限制为1毫秒:如果在每1毫秒的GC暂停之间仅允许应用程序运行0.1毫秒,那么将不会取得什么进展,甚至微不足道的RT系统也可能会失败因为他们没有时间进步。 实际上,足够短的暂停时间与完整的STW GC一样。
图2说明了一个场景,其中GC在大部分时间中运行,但仍保留1毫秒的暂停时间:
除了有限制的暂停时间外,还需要采取其他措施,以便为分配给应用程序和GC的时间百分比提供一定程度的确定性。 我们将应用程序利用率定义为在给定的时间范围内分配给应用程序的时间百分比,该时间百分比在应用程序的完整运行中连续滑动。 节拍器保证一定比例的处理时间专用于该应用程序。 剩余时间的使用由GC自行决定:可以分配给应用程序,也可以由GC使用。 与传统的收集器相比,较短的暂停时间可提供更细粒度的使用保证。 由于用于测量利用率的时间间隔接近零,因此应用程序的预期利用率为0%或100%,因为测量值低于GC量子大小。 使用的保证严格取决于滑动窗的尺寸。 节拍器在10毫秒的窗口中使用的长度为500微秒,其默认利用率目标为70%。
图3说明了一个GC周期,该周期分为多个500微秒的时间片,可在10毫秒的窗口内保持70%的利用率:
在图3中,每个时间片代表一个运行GC或应用程序的量子。 时间片下方的条形表示滑动窗口。 对于任何滑动窗口,最多有6个GC量子和至少14个应用量子。 每个GC量之后至少要有1个应用量,即使目标使用率可以通过背靠背GC量来保留。 这样可确保将应用程序暂停时间限制为1个量子长度。 但是,如果将目标利用率指定为低于50%,则会发生某些背靠背GC定量的情况,以使GC能够跟上分配。
图4和5展示了典型的应用程序使用情况。 在图4中,利用率降至70%的区域表示正在进行的GC周期的区域。 请注意,当GC不活动时,应用程序利用率为100%。
图5仅显示了图4的GC循环分数:
图5的部分A是阶梯图,其中下降部分对应于GC量子,而平坦部分对应于应用量子。 阶梯展示了GC通过与应用程序进行交互来尊重低暂停时间,并朝着目标利用率产生了阶梯状的下降。 B节仅由应用程序活动组成,以保留所有滑动窗口中的利用率目标。 常见的情况是,利用率模式仅在模式开始时显示GC活动。 发生这种情况的原因是,GC在允许的任何时候都会运行(保留暂停时间和利用率),这通常意味着它会在模式开始时用尽分配的时间,并允许应用程序在剩余的时间范围内恢复。 C部分说明了利用率接近目标利用率时的GC活动。 上升部分代表应用程序数量,下降部分代表GC数量。 该部分的锯齿性质再次是由于GC和应用程序的交错,以保持较低的暂停时间。 D部分代表GC循环完成之后的部分。 本部分的升序性质说明了GC不再运行且应用程序将重新获得100%利用率的事实。
目标利用率在节拍器中由用户指定; 您可以在本文的调音节拍器部分中找到更多信息。
节拍器旨在为现有应用程序提供RT行为。 无需修改用户代码。 必须根据应用程序调整所需的堆大小和目标利用率,以便目标利用率可以保持所需的应用程序吞吐量,同时让GC跟上分配的步伐。 用户应在他们希望承受的最大负载下运行其应用程序,以确保保留RT特性并确保应用程序吞吐量足够。 本文的“ 调音节拍器”部分介绍了在吞吐量或利用率不足时可以采取的措施。 在某些情况下,Metronome的短暂停时间保证不足以满足应用程序的RT特性。 对于这些情况,可以使用RTSJ避免GC引起的暂停时间。
RTSJ是“对Java平台的补充规范,以使Java程序可用于实时应用程序。” 节拍器必须了解RTSJ的某些方面-特别是RealtimeThread
(RT线程), NoHeapRealtimeThread
(NHRT)和不朽内存 。 RT线程是Java线程,除其他特征外,它们的运行优先级高于常规Java线程。 NHRT是RT线程,不能包含对堆对象的引用。 换句话说,NHRT可访问的对象不能引用受GC约束的对象。 作为这种折衷的交换,即使在GC周期内,GC也不会妨碍NHRT的调度。 这意味着NHRT不会产生任何暂停时间。 不朽的内存提供不受GC约束的内存空间; 这意味着NHRT可以引用不朽的物体。 这些只是RTSJ的某些方面。 有关完整规范的链接,请参见参考资料 。
Metronome在J9虚拟机中使用了几种关键方法来实现确定性的暂停时间,同时保证GC的安全性。 其中包括arraylet,基于时间的垃圾收集器调度,用于跟踪活动对象的根结构处理,在J9虚拟机和GC之间进行协调以确保找到所有活动对象,以及用于将J9虚拟机挂起的机制。 GC量子。
尽管Metronome通过将收集过程分解为递增的工作单元来获得确定的暂停时间,但是在某些情况下分配可能会导致GC出现故障。 一个领域是大型对象的分配。 对于大多数收集器实现,分配子系统保留一个空闲堆内存池,这些内存由应用程序通过分配对象消耗,并由收集器通过清除来补充。 在第一个收集之后,空闲堆内存主要是曾经存在但现在已经死亡的对象的结果。 因为没有关于这些对象如何死亡或何时死亡的可预测模式,所以即使相邻死对象发生合并,堆上产生的可用内存也是大小不同的碎片块的集合。 此外,每个收集周期可以返回不同的空闲块模式。 结果,如果没有足够的可用内存块来满足请求,则分配足够大的对象可能会失败。 通常,这些大对象是数组。 标准对象通常不超过几十个字段,大多数JVM的大小通常不足2K。
为了减轻碎片问题,一些收集器在其收集周期中实施了压缩或碎片整理阶段。 清除完成后,如果无法满足分配请求,系统将尝试在堆中四处移动现有活动对象,以将两个或更多空闲块合并为一个更大的块。 此阶段有时被实现为按需功能,可以嵌入到收集器的结构中(以半空间收集器为例),也可以以增量方式实现。 这些系统中的每一个都有其折衷,但是通常,压缩阶段在时间和精力上是昂贵的。
WebSphere Real Time中最新版本的Metronome并未实现压缩系统。 为了防止出现碎片问题,Metronome使用arraylet ,它将标准的线性表示分解为几个可以相互独立分配的离散片段。
图6显示了数组对象显示为脊线(它是中心对象,并且是堆上其他对象可以引用的唯一实体)以及一系列arraylet 叶子 ,其中包含实际的数组内容:
arraylet叶子未被其他堆对象引用,并且可以以任何位置和顺序散布在整个堆中。 叶子的大小固定,可以简单地计算元素位置,这是间接添加的。 如图6所示,由于在主干中包含内部碎片,已经优化了内存使用开销,方法是在主干中包含叶子的任何尾随数据。
请注意,这种格式可能意味着阵列主干可以增长到无限制的大小,但是在现有系统中尚未发现这是一个问题。
为了调度GC的确定性暂停,Metronome使用两个不同的线程来实现一致的调度和较短的,不间断的暂停时间:
尽管Metronome使用一系列小的增量暂停来完成GC周期,但它仍必须以STW方式为每个数量级暂停JVM。 对于每个这些STW暂停,Metronome在J9虚拟机中使用协作暂停机制 。 该机制不依赖任何特殊的本机线程功能来挂起线程。 相反,它使用异步样式的消息传递系统来通知Java线程,它们必须释放对内部JVM结构(包括堆)的访问权,并进入睡眠状态,直到收到发出恢复处理的信号为止。 J9虚拟机中的Java线程会定期检查是否已发出挂起请求,如果已发出,则它们按以下步骤进行:
恢复后,线程将重新读取对象指针,并重新获取它们先前持有的与JVM相关的结构。 释放JVM结构的行为使GC线程可以安全的方式处理这些结构。 读取和写入部分更新的结构可能会导致意外行为和崩溃。 通过存储然后重新加载对象指针,线程使GC有机会在GC范围内更新对象指针,如果将对象作为任何类似于压缩的操作的一部分进行移动,则这是必需的。
因为挂起机制与Java线程配合使用,所以每个线程中的定期检查必须以尽可能短的间隔间隔开,这一点很重要。 这是JVM和即时(JIT)编译器的责任。 尽管检查挂起请求会带来开销,但它可以根据GC的需求很好地定义堆栈之类的结构,从而可以准确确定堆栈中的值是否是指向对象的指针。
此挂起机制仅用于当前参与JVM相关活动的线程; 非Java线程,或Java本机接口(JNI)代码中未使用JNI API的Java线程,不会被暂停。 如果这些线程参与任何JVM活动(例如,附加到JVM或调用JNI API),它们将协作挂起,直到GC数量完成。 这很重要,因为它允许继续调度与Java进程关联的线程。 而且,尽管将优先考虑线程优先级,但是在其他线程中以任何明显的方式干扰系统可能会影响GC的确定性。
完整的STW收集器的优点是能够跟踪对象引用和JVM内部结构,而应用程序不会干扰对象图中的链接。 通过将GC周期划分为一系列小的STW阶段并将其执行与应用程序进行交错,Metronome确实在跟踪系统中的活动对象方面引入了潜在的问题。 发生意外的行为或崩溃是因为应用程序在处理对象之后可以修改对象的引用,从而使未处理的对象对收集器隐藏。 图7说明了隐藏对象问题:
假设对象图存在于堆中,如图7中第I部分所述。节拍器收集器处于活动状态,并已计划在此量子范围内执行跟踪工作。 在其指定的时间段内,它设法跟踪根对象及其引用的对象,然后再用完时间并需要将JVM调度回第二节。 在应用程序运行期间,对象之间的引用会更改,以使对象A现在指向未处理的对象,该对象在III节中不再被任何其他位置引用。 然后,将GC重新安排到另一个量子中并继续处理,但缺少此隐藏的对象指针。 结果是,在GC的清除阶段将未标记的对象返回到空闲列表中,活动对象将被回收,从而导致指针悬空 ,从而导致错误行为,甚至在JVM或GC中崩溃。
为了防止此类错误,JVM和Metronome必须合作跟踪对堆和JVM结构的更改,以使GC保持所有相关对象处于活动状态。 这是通过写屏障实现的, 写屏障可跟踪对对象的所有写入并记录对象之间引用的创建和中断,以便收集器可以跟踪潜在的隐藏活动对象。 节拍器使用的屏障类型称为快照快照 (SATB)。 它从概念上记录了收集周期开始时堆的状态,并保留了该时刻的所有活动对象以及当前周期中分配的所有活动对象。 具体解决方案涉及汤浅型势垒(参照相关信息 ),其中在任何场存储器被覆盖值被记录和处理为如果它有与之相关联的根引用。 在覆盖之前保留插槽的原始值可使活动对象集得以保留和处理。
内部JVM结构(包括“ JNI全局参考”列表)也需要这种类型的屏障处理。 由于应用程序可以在此列表中添加和删除对象,因此将应用屏障来跟踪两个删除的对象,以避免出现隐藏对象问题(类似于字段覆盖),并添加了对象以消除重新扫描结构的需要。
为了开始跟踪活动对象,垃圾收集器从从roots获得的一组初始对象开始。 根是JVM中的结构,表示对应用程序创建的对象的硬引用,这些对象是显式创建的(例如,JNI全局引用)或隐式创建的(例如,堆栈)。 根结构被扫描为收集器中标记阶段初始功能的一部分。
在执行过程中,大多数根在其对象引用方面具有一定的可塑性。 因此,如我们在Write barriers中所讨论的,必须跟踪对其参考集的更改。 但是,某些结构(例如堆栈)在没有对性能造成重大损失的情况下无法承受推入和弹出操作的跟踪。 因此,为节拍器做出某些限制和更改,以符合节拍式障碍:
了解堆大小和应用程序利用率之间的相关性很重要。 尽管高目标利用率对于实现最佳应用程序吞吐量是理想的,但是GC必须能够跟上应用程序分配率。 如果目标利用率和分配率都很高,则应用程序可能会用完内存,从而迫使GC连续运行,并且在大多数情况下将利用率降至0%。 这种降级会导致较大的暂停时间,这对于RT应用程序通常是不可接受的。 如果遇到这种情况,则必须选择降低目标利用率以允许更多的GC时间,增加堆的大小以允许更多的分配,或者两者结合。 某些情况下可能没有维持某个利用率目标所需的内存,因此以性能成本降低目标利用率是唯一的选择。
图8说明了堆大小和应用程序利用率之间的典型权衡。 较高的利用率百分比需要较大的堆,因为不允许GC在较低的利用率允许的范围内运行。
利用率和堆大小之间的关系高度依赖于应用程序,要达到适当的平衡,需要对应用程序和VM参数进行反复试验。
详细GC是一种将GC活动记录并输出到文件或屏幕的工具。 您可以使用它来确定参数(堆大小,目标利用率,窗口大小和量子时间)是否支持正在运行的应用程序。 清单1显示了详细输出的示例:
每个详细GC事件都包含在
标记内。 可以使用各种事件类型,但最常见的事件类型包含在清单1中synchgc
类型表示同步GC,它是一个从头到尾连续运行的GC周期; 也就是说,没有发生与应用程序的交错。 发生这种情况有两个原因:
System.gc()
由应用程序调用。
标记中包含同步GC的原因包括第一种情况的system garbage collect
,第二种情况out of memory
。 第一种情况与指定参数的应用程序的可持续性无关。 但是,在许多情况下,从用户应用程序调用System.gc()
会导致应用程序利用率降至0%,并且会导致较长的暂停时间。 因此应该避免。 但是,如果由于第二种情况而发生同步GC(内存不足错误),则意味着GC无法跟上应用程序分配。 因此,您应考虑增加堆或降低应用程序使用率目标,以避免发生同步GC。
trigger
GC事件类型对应于GC周期的起点和终点。 它们对于分隔heartbeat
GC事件的批次很有用。 heartbeat
GC事件类型将多个GC量子的信息汇总为一个汇总的详细事件。 请注意,这与警报线程心跳无关。 量子quantumcount
属性对应于heartbeat
GC中累积的GC量子数量。
标签表示有关heartbeat
GC中汇总的GC量子的时序信息。
和
标记包含有关heartbeat
GC中累积的量子末尾的可用内存的信息。
标记包含有关量子开始时GC线程优先级的信息。
量子时间值对应于应用程序看到的暂停时间。 平均量子时间应接近500微秒,并且必须监控最大量子时间,以确保它们落在RT应用程序可接受的暂停时间内。 当GC由其他进程系统中的抢占,可能会发生大的暂停时间,防止它完成其量子并允许应用程序恢复时,或者当系统中的某些根结构被滥用并生长至无法管理的大小(参见要考虑的问题使用节拍器时 )。
永久内存是RTSJ所需的资源,不受GC限制。 因此,在冗长的GC日志中看到永生的可用内存下降而没有恢复是很正常的。 它用于诸如字符串常量和类之类的对象。 您需要了解程序的行为,并适当调整不朽内存的大小。
您应该监视堆使用情况,以确保总体趋势保持稳定。 堆可用空间的下降趋势将表明存在由应用程序引起的潜在泄漏。 多种情况可能导致泄漏,包括不断扩大的哈希表,无限期保留的大型资源对象以及未清除全局JNI引用。
图9和10说明了自由堆空间的稳定和下降趋势。 请注意,局部最小值和最大值是正常的和预期的,因为可用空间仅在GC周期内增加,而在应用程序活动和分配时相应地减少。
标记的interval
属性对应于自从输出相同类型的最后一个详细GC事件以来经过的时间。 In the case of the heartbeat
event type, it can represent the time since the trigger start
event if it's the first heartbeat for the current GC cycle.
Tuning Fork is a separate tool for tuning Metronome to suit the user application better. Tuning Fork lets the user inspect many details of GC activity either after the fact through a trace log or during run time through a socket. Metronome was built with Tuning Fork in mind and logs many events that can be inspected from within the Tuning Fork application. For example, it displays the application utilization over time and inspects the time taken for various GC phases.
Figure 11 shows the GC performance summary graph generated by Tuning Fork, including target utilization, heap memory use, and application utilization:
Metronome strives to deliver short deterministic pauses for GC, but some situations arise both in application code and the underlying platform that can perturb these results, sometimes leading to pause-time outliers. Changes in GC behavior from what would be expected with a standard JDK collector can also occur.
The RTSJ states that GC doesn't process immortal memory. Because classes live in immortal memory, they are not subject to GC and therefore can't be unloaded. Applications expecting to use a large number of classes need to adjust immortal space appropriately, and applications that require class unloading need to make adjustments to their programming model within WebSphere Real Time.
GC work in Metronome is time based, and any change to the hardware clock could cause hard-to-diagnose problems. An example is synchronizing the system time to a Network Time Protocol (NTP) server and then synchronizing the hardware clock to the system time. This would appear as a sudden jump in time to the GC and could cause a failure in maintaining the utilization target or possibly cause out-of-memory errors.
Running multiple JVMs on a single machine can introduce interference across the JVMs, skewing the utilization figures. The alarm thread, being a high-priority RT thread, preempts any other lower-priority thread, and the GC thread also runs at an RT priority. If sufficient GC and alarm threads are active at any time, a JVM without an active GC cycle might have its application threads preempted by another JVM's GC and alarm threads while time is actually taxed to the application because the GC for that VM is inactive.
翻译自: https://www.ibm.com/developerworks/java/library/j-rtj4/index.html