实时Java,第4部分-实时垃圾收集

实时系统和垃圾收集

实时(RT)应用程序开发与通用应用程序开发的差异在于前者对部分运行时行为强加了时间限制。此类限制通常是对应用程序的某些部分实施的,比如中断处理程序,其响应中断的代码必须在给定的时间范围内完成工作。对于硬 RT 系统,比如心脏监测器或国防系统,如果这类系统的运行超出时限,可以看作是整个系统的灾难性失败。而对于软 RT 系统,超出时限可能会有些不利影响 —— 比如 GUI 不能显示其监控流的所有结果 —— 但是不会导致系统失败。

在 Java 应用程序中,Java 虚拟机(JVM)负责优化运行时行为、管理对象堆以及接合操作系统和硬件。虽然语言和平台之间的这个管理层简化了软件开发,但同时也给程序带来了一定数量的开销。GC 就是一个这样的例子,它通常会导致应用程序中的不确定性暂停。暂停的频率和时长都不可预测,使得 Java 语言在传统上并不适合开发 RT 应用程序。一些基于 Java 实时规范(RTSJ)的现有解决方案使开发人员能够避开 Java 技术的不确定性方面,但是需要对现有的编程模型做些更改。

Metronome 是一种确定性的垃圾收集器,为标准的 Java 应用程序提供有限制的低暂停时间和指定的应用程序利用率。有限制的暂停时间的减少源于收集方法的增加和细致的工程决断,包括对 VM 的基本更改。利用率是指应用程序所能够运行的特定时间窗中的时间百分比,剩余时间则用于 GC。Metronome 让用户能够指定应用程序的利用率级别。通过与 RTSJ 结合使用,Metronome 使开发人员能够在时间窗很小的情况下构建具有确定的低暂停时间和无暂停的软件。本文解释了 RT 应用程序的传统 GC 的限制,详述了 Metronome 的方法,并且为使用 Metronome 开发硬 RT 应用程序提供了一些工具和指导。

传统 GC

传统 GC 实现使用 stop-the-world (STW) 方法来恢复堆内存。应用程序一直运行,直至耗尽堆的可用内存,此时 GC 停止所有的应用程序代码、执行垃圾收集,然后让应用程序继续运行。

图 1 演示了用于 GC 活动的传统的 STW 暂停,这些暂停在频率和持续时间方面通常都不可预测。传统的 GC 是不确定的,因为恢复内存所需执行工作的数量取决于应用程序所使用对象的总的数量和大小、这些对象之间的相互连接,以及为释放足够的堆内存以满足未来分配所需完成的工作的多少。

图 1. 传统 GC 暂停

传统 GC 非确定性的原因

考察一下 GC 的基本组件,就不难理解 GC 时间没有限制并且不可预测的原因了。GC 暂停通常包括两个独立阶段:标记 阶段和清理 阶段。虽然很多实现和方法可以结合或修改这两个阶段的含义,或通过其他手段(如压缩或减少堆中的碎片)来增强 GC,或使某些阶段的操作与应用程序的运行并发执行,这两个概念是传统 GC 的技术基线。

标记阶段负责跟踪应用程序可见的所有对象并将它们标记 为活的,以免回收它们的存储。这个跟踪以根集 开始,它由一些内部结构组成,比如线程栈和对象的全局引用。跟踪然后遍历引用链直至标记完根集中所有(直接或间接)可获得的对象。标记阶段最后也没有标记的对象是应用程序不可获得的对象(死对象),因为不存在从根集经过任何引用序列找到这些对象的路径。标记阶段的长度不可预测,原因是应用程序中活对象的数目在任何特定时间都不可预测,并且遍历所有引用以便找到系统中所有活对象的耗费也不可预测。一个运行稳定的系统中的 oracle 可以根据以前的计时特征来预测时间需求,但是这些预测的精确性又是不确定性的另一个来源。

清理阶段负责在标记完成后考察堆并回收死对象的存储,将其放回堆的自由存储中,使那些存储可用于分配。与标志阶段类似,将死对象清理回自由内存池的耗费也不能完全预测。虽然系统中活对象的数目和大小可从标记阶段获得,但是对它们在堆中的位置及其对于自由内存池的适宜性进行分析所需的工作却不可预测。

传统 GC 对于 RT 应用程序的适宜性

RT 应用程序必须能够响应具有确定时间间隔的实际刺激。传统 GC 无法满足这个需求,因为应用程序必须暂停以便 GC 可以回收所有未使用的内存。回收所花费的时间没有限制并受波动的影响。此外,GC 中断应用程序的时机在传统上不可预测。应用程序暂停的持续时间被称作暂停时间,因为这段时间暂停了应用程序进程使 GC 可以回收自由空间。RT 应用程序要求低暂停时间,因为那通常表示应用程序响应具有较高的计时限制。

Metronome GC

Metronome 的方法用于将执行 GC 循环的时间划分为一系列的增量,称作量子。为此,每个阶段通过一系列不连续的步骤来完成其全部工作,允许收集器执行以下操作:

抢占应用程序一小段确定的时间。

执行收集操作。

让应用程序恢复运行。

这个顺序与传统模型形成了很好的对比,在传统模型中,应用程序在某个不可预测的时间暂停,GC 运行一段没有限制的时间后完成,然后 GC 停止,让应用程序恢复运行。

虽然将 STW GC 循环分解为短暂的有限制的暂停有助于减少 GC 的影响,但这对 RT 应用程序来说还不够。为了使 RT 应用程序满足其时限要求,任何特定时间段中都必须有足够的部分可供应用程序使用;否则,就会发生需求冲突,应用程序也会失败。例如,可假定一个 GC 暂停被限制为 1 毫秒的场景:在每 1 毫秒的 GC 暂停之间,应用程序只能运行 0.1 毫秒,则程序执行几乎没有进展,甚至不太复杂的 RT 系统也可能失败,因为它们缺少时间执行处理。实际上,足够接近的短暂停时间与完整的 STW GC 并无二致。

图 2 演示了 GC 运行多数时间但仍然保留 1 毫秒的暂停时间:

图 2. 暂停时间短但是应用程序时间更短

利用率

除了有限制的暂停时间外,还需要另外一种测量方法来确定分配给应用程序和 GC 的时间百分比。我们将应用程序利用率定义为:分配给特定时间窗内的应用程序的时间百分比,在这段时间内应用程序连续地执行完整个运行过程。Metronome 保证应用程序可以获得一定百分比的处理时间。剩余时间的使用由 GC 决定:可以分配给应用程序也可以由 GC 使用。短暂停时间可以保证比传统收集器分解得更细的利用率。因为用于测量利用率的时间间隔趋近于零,所以应用程序的预期利用率是 0% 或 100%,原因是这个时间间隔低于 GC 量的大小。对滑动窗口大小的度量必须严格保证利用率。Metronome 在 10 毫秒的时间窗内使用 500 微秒的时间量,默认的利用率目标为 70%。

图 3 演示一个划分为多个 500 微秒时间片的 GC 循环,该循环在 10 毫秒的时间窗内具有 70% 的利用率:

图 3. 滑动窗口利用率

在图 3 中,每个时间片表示运行 GC 或应用程序的一个时间量。时间片下面的各栏表示滑动窗口。每个滑动窗口具有最多 6 个 GC 时间量和至少 14 个应用程序时间量。每个 GC 时间量后接至少 1 个应用程序时间量,即使通过连续的 GC 时间量来保持目标利用率也是如此。这就保证了将应用程序暂停时间限制为 1 个时间量长度。但是,如果指定的目标利用率低于 50%,则会产生一些连续的 GC 时间量使 GC 能够满足分配。

图 4 和图 5 演示了一个典型的应用程序利用率场景。在图 4 中,利用率降为 70% 的区域表示正在进行 GC 循环的区域。注意,如果 GC 是不活动的,则应用程序的利用率为 100%。

图 4. 总利用率

图 5 演示了图 4 的一个 GC 循环片段:

图 5. GC 循环利用率

图 5 的 A 段是一个梯型图,其中下降的部分对应于 GC 时间量,而平缓的部分对应于应用程序时间量。梯型表示 GC 通过与应用程序交错实现低暂停时间,从而产生目标利用率的梯状下降。组成 B 段的应用程序活动只保持所有滑动窗口的利用率目标。利用率模式只在模式开始时显示 GC 活动,这一点很常见。原因在于只要得到允许,GC 就会运行(保持暂停时间和利用率),而这通常意味着它会在模式开始时耗尽分得的时间并允许应用程序在时间窗的剩余部分恢复执行。C 段表示利用率接近目标利用率时的 GC 活动。上升的部分表示应用程序时间量,而下降的部分表示 GC 时间量。造成此段呈锯齿状的原因同样是因为 GC 和应用程序交错执行以便保持低暂停时间。D 段表示 GC 循环完成前的部分。此段呈上升趋势表示 GC 不再运行而应用程序将重新获得 100% 的利用率。

在 Metronome 中,用户可以指定目标利用率;本文的 调整 Metronome 一节提供了与此相关的更多信息。

使用 Metronome 运行应用程序

Metronome 的设计目的是为现有的应用程序提供 RT 行为。不需要修改用户代码。期望的堆大小和目标利用率必须针对应用程序进行调整,使目标利用率保持期望的应用程序吞吐量,同时使 GC 能够满足分配。用户应该按照希望维持的最大负载运行应用程序,以便保证 RT 特征和充足的应用程序吞吐量。本文的 调整 Metronome 一节说明了吞吐量或利用率不足时可以执行哪些操作。在某些情形下,Metronome 的短暂停时间保证不能满足应用程序的 RT 特征。此时,您可以使用 RTSJ 来避免 GC 导致的暂停时间。

Java 实时规范

RTSJ 是 “一种使 Java 程序能够用于实时应用程序的 Java 平台规范”。Metronome 必须意识到 RTSJ 的某些方面 —— 尤其是 RealtimeThread(RT 线程)、NoHeapRealtimeThread(NHRT)和永久内存。除了别的特征外,RT 线程是以高于普通 Java 线程的优先级运行的 Java 线程。NHRT 是不能包含堆对象引用的 RT 线程。换言之,NHRT 能够访问的对象不能引用服从 GC 的对象。作为对这种妥协的交换,GC 不会阻止 NHRT 的调度,即使在 GC 循环期间也是如此。这意味着 NHRT 不会导致任何暂停时间。永久内存提供了一个不服从 GC 的内存空间;即,NHRT 可以引用永久对象。这些只是 RTSJ 的一些方面,有关完整规范的链接,请参阅 参考资料。

确定性 GC 相关的技术问题

Metronome 使用 J9 虚拟机中的几个关键方法来实现确定的暂停时间,同时保证 GC 的安全性。这些方法包括 arraylet、基于时间的垃圾收集器调度、用于跟踪活对象的根结构处理、协调 J9 虚拟机和 GC 以保证能够找到所有的活对象,以及用于暂停 J9 虚拟机来提供 GC 时间量的机制。

Arraylet

虽然 Metronome 通过将收集过程分解为步进的工作单元实现了确定的暂停时间,但是在某些情形下分配可能导致 GC 中出现 hiccup。大对象的分配就是一个这样的例子。对大多数收集器实现而言,分配子系统持有一个自由堆内存池,应用程序通过分配对象使用该池,然后由收集器通过清理来补充该池。第一次收集后,自由堆内存主要是一些曾经的活对象(现在已死)的结果。因为没有关于这些对象如何死去或何时死去的可预测模式,所以得到的堆上的自由内存是大小不一的碎片集合,即使会出现相邻死对象合并的情况。此外,每个收集循环会返回一个不同的自由块模式。结果,如果没有足够大的自由内存块能够满足请求的需要,则分配一个很大的对象就会失败。这些大对象通常是数组;标准对象一般不会多于几十个字段,在大多数 JVM 中常常占用不到 2K 的空间。

为了缓解碎片问题,一些收集器针对其收集循环实现一个压缩或碎片整理阶段。清理完成后,如果分配请求无法满足,则系统将尝试移动堆中现有的活对象以便将两个或更多的自由块合并成一个更大的块。这个阶段有时作为一个随需应变的特性来实现,被嵌入到收集器的结构(例如半空间收集器)中,或以一种增量的形式来实现。每个这样的系统都有自己的平衡方法,但一般说来压缩阶段在时间和工作上都耗费颇多。

WebSphere Real Time 中当前版本的 Metronome 没有实现压缩系统。为使碎片不成为一个问题,Metronome 使用 arraylet 将标准的线性表示分解为若干个不连续的小块,可以对这些小块进行彼此独立的分配。

图 6 演示了数组对象作为 spine(它是可由堆上的其他对象引用的中心对象和惟一实体)和一系列的 arraylet 叶子(包含有实际的数组内容)出现:

图 6. Arraylet

arraylet 叶子不由其他的堆对象引用,并且可能在堆中的任何位置以任意顺序分布。这些叶子具有固定的大小,允许对元素的位置进行简单的计算,这是一个附加的迂回。如图 6 所示,spine 中由于内部碎片导致的内存使用开销已经通过将叶子的所有 trailing 数据包含到 spine 中而得到优化。

注意,这种格式意味着数组 spine 可能成长到无限制的大小,但是在现有的系统中还没有发现这是一个问题。

调度 GC 时间量

为了给 GC 调度确定性暂停,Metronome 使用了以下两个不同的线程来完成一致性调度和短暂连续的暂停时间:

alarm 线程。为了确定地调度 GC 时间量,Metronome 使用 alarm 线程来用作心跳机制。alarm 线程具有很高的优先级(比系统中所有其他的 JVM 线程的优先级都要高),它的唤醒速度与 GC 量子时间段相同(Metronome 中为 500 微秒),并负责决定是否应该调度某个 GC 时间量。如果应该调度,则 alarm 线程必须暂停运行 JVM 并唤醒 GC 线程。alarm 线程只在一个很短的时间段内处于活动状态(通常低于 10 微秒)并随应用程序静默的执行。

GC 线程。GC 线程在一个 GC 时间量期间执行实际工作。GC 线程必须首先完成对 alarm 线程启动的 JVM 的暂停。然后才能在剩余的时间内执行 GC 工作,在时间量临近结束时将自身调回休眠状态并恢复 JVM 的运行。如果 GC 线程无法在时间量结束前完成预定的任务项目,那么也可以抢占性地进入休眠。对于 RTSJ 而言,GC 线程的优先级比除 NHRT 之外的所有 RT 线程的优先级都要高。

协作暂停机制

虽然 Metronome 使用一系列小的、步进式的暂停来完成一个 GC 循环,但是它仍然必须以 STW 方式为每个时间量暂停 JVM。对于每个这样的 STW 暂停,Metronome 在 J9 虚拟机中使用协作暂停机制。这个机制不依赖任何特殊的本地线程功能来暂停线程。作为替代,它使用了一个异步形式的消息传递系统来通知 Java 线程:必须释放对内部 JVM 结构(包括堆)的访问并进入休眠,直至被告知恢复处理。J9 虚拟机中的 Java 线程周期性地检查是否发出了暂停请求,如果已经发出,则它们将执行以下步骤:

释放所有使用的内部 JVM 结构。

将所有使用的对象引用存储在良好描述的位置。

告知中央 JVM 暂停机制已经到达安全点。

休眠并等待相应的恢复。

一旦恢复,线程将重新读取对象指针并重新获取其先前占用的 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. 隐藏对象问题

假定图 7 第 I 段所描述的堆中存在一个对象图。Metronome 收集器处于活动状态并在此时间量中被分配执行跟踪工作。在其分配的时间段中,它的作用是在时间用完之前跟踪根对象及其引用的对象,并且需要将 JVM 调回 II 段。在应用程序的运行期间,对象之间的引用会发生变化,因此对象 A 现在指向一个未处理对象,该对象不再被 III 段中的任何其他位置引用。然后 GC 被调回并在其他的时间量中继续处理,漏掉这个隐藏 对象指针。结果是,在把未标记对象返回到自由列表的 GC 的清理阶段,将会回收一个活对象,产生一个悬空 指针,导致不正确的行为或者甚至是 JVM 或 GC 中的破坏。

为了防止出现此类错误,JVM 和 Metronome 必须协作跟踪堆和 JVM 的更改,使 GC 将所有的相关对象保持为活的。这项任务通过写入屏障 来完成,它将跟踪所有的对象写操作并记录对象间引用的创建和销毁,使收集器可以跟踪潜在隐藏的活对象。Metronome 所使用的屏障类型称为初始快照(snapshot at the beginning,SATB)屏障。它在收集循环开始时在概念上记录堆的状态并保留当时的和当前循环中分配的所有活对象。具体的解决方案涉及一个 Yuasa 类型的屏障(请参阅 参考资料),其中将会记录任何字段存储中的重写值并将其视为具有相关的根引用。在重写启用活对象设置保护和处理之前保留槽的原始值。

内部 JVM 结构也需要此类屏障处理,包括 JNI 全局引用列表结构。因为应用程序可以向此列表中添加对象和从中删除对象,所以可以使用屏障来跟踪删除的对象(避免类似于字段重写的隐藏对象问题)和添加的对象(消除重新扫描结构的需求)。

根扫描和根处理

为了开始跟踪活对象,垃圾收集器从一组根 中获得的初始对象开始。根是 JVM 中的结构,表示了应用程序显式(如 JNI 全局引用)创建或隐式(如栈)创建的对象的硬引用。根结构被作为收集器中标记阶段初始功能的一部分进行扫描。

大多数根在执行期间可以根据其对象引用进行延展。出于这个原因,必须跟踪对它们的引用设置的更改,如 写入屏障 一节所述。但是,某些结构(如栈)不能提供对未造成严重性能影响的 push 和 pop 的跟踪。因此,对扫描栈作出了某些限制和更改,使 Metronome 能够适合 Yuasa 形式的屏障:

栈的原子扫描。单独线程栈必须自动进行扫描,或在单个时间量内扫描。这样做的原因在于,执行期间线程可以从其栈中弹出任意数量的引用 —— 执行过程中可能存储在其他位置的引用。栈扫描过程中的暂停可能导致丢失对存储的跟踪或在两部分扫描之间错过,在堆中产生一个悬空指针。应用程序开发人员应该意识到栈应自动进行扫描并且应避免在 RT 应用程序中使用很深的栈。

模糊屏障。虽然栈必须自动进行扫描,但是如果在单个时间量期间所有栈都被扫描,则可能难以保持确定性。GC 和 JVM 可以在扫描 Java 栈的同时交错执行。这可能导致通过一系列加载和存储将对象从一个线程移动到另一个线程。为了避免丢失对象的引用,GC 期间未被扫描过的线程让屏障跟踪重写值和存储的值。跟踪存储的对象,应将其存储在已经处理过的对象中并从栈中弹出,通过写入屏障保持可获取性。

调整 Metronome

了解堆大小和应用程序利用率方面的关系非常重要。虽然高目标的利用率对于实现最佳应用程序吞吐量很有帮助,但是 GC 必须能够跟上应用程序的分配率。如果目标利用率和分配率都很高,则应用程序可能耗尽内存,强迫 GC 连续地运行并且在多数情况下使利用率降低到 0%。这种降低带来了大量的暂停时间,通常对 RT 应用程序来说不可接收。如果遇到这种情形,必须作出选择来降低目标利用率,以便提供更多的 GC 时间,增加堆大小以支持更多的应用程序,或将此二者结合使用。某些情形可能需要使用内存以维持确定的目标利用率,因此在性能开销上降低目标利用率是惟一选择。

图 8 演示了一种典型的堆大小和应用程序利用率之间的平衡。更高的利用率百分比需要更大的堆,因为有些堆在低利用率的应用程序中允许运行,而在 GC 中则不允许运行。

图 8. 堆大小和利用率的对比

利用率和堆大小之间的关系跟应用程序有很大关系,达到一个大致平衡需要使用应用程序和 VM 参数反复实验。

冗余 GC

冗余 GC 是一种记录 GC 活动并将其输出到一个文件或屏幕中的工具。您可以使用它来确定参数(堆大小、目标利用率、窗口大小和时间量)是否支持应用程序运行。清单 1 演示了一个冗余输出的例子:

清单 1. 冗余 GC 示例

 

             

             

 

             

每个 Verbose 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 时间量的信息整合到一个总括的冗余事件中。注意,这与 alarm 线程心跳无关。quantumcount 属性对应 heartbeat GC 中整合的 GC 时间量。 标记表示关于 heartbeat GC 中整合的 GC 时间量的计时信息。 和 标记包含关于 heartbeat GC 中整合的时间量结尾的自由内存的信息。 标记包含关于时间量开始时 GC 线程优先级的信息。

时间量值指的是应用程序所见的暂停时间。普通的时间量接近 500 微秒,而且必须对最大的时间量进行监控以保证它们能够为 RT 应用程序提供可接受的暂停时间。长暂停时间可能源于 GC 被系统中的其他进程抢占,使其不能完成其时间量而且不能让应用程序恢复运行,或者是因为滥用了系统中的某些根结构并成长到了不可管理的大小(见 使用 Metronome 时的注意事项 一节)。

永久内存是 RTSJ 所需要的并且不服从 GC 的资源。出于这个原因,常常会发现冗余 GC 记录中的永久自由内存下降并且不会恢复。它用于诸如字符串常量和类之类的对象。需要注意程序的行为并适当地调整永久内存的大小。

应该监控堆的使用以保证总趋势保持稳定。堆自由空间呈下降趋势表明可能存在应用程序导致的泄漏。导致泄漏的原因很多,包括越来越多的散列表、不确定保存的大型资源对象和未清理的全局 JNI 引用。

图 9 和图 10 演示了自由堆空间中的稳定和递减的趋势。注意,容易实现本地的最小和最大空间,因为自由空间仅在 GC 循环期间增加并且在应用程序处于活动时和进行分配时相应减少。

图 9. 稳定自由堆

图 10. 递减自由堆

标记的 interval 属性指的是从上次输出相同类型的冗余 GC 事件起所经过的时间。对于 heartbeat 事件类型,它用来表示从 trigger start 事件起所经过的时间(如果它是当前 GC 循环的第一次心跳)。

Tuning Fork

Tuning Fork 是一种独立的工具,用于调优 Metronome 以便更好地适合用户应用程序。Tuning Fork 让用户检查 GC 活动的很多细节,方法是在执行活动后通过跟踪日志检查或在运行时通过套接字检查。Metronome 构建时考虑了 Tuning Fork 并记录了很多可在 Tuning Fork 应用程序中检查的事件。例如,它显示了时间上的应用程序利用率并检查了用于各个 GC 阶段的时间。

图 11 演示了 Tuning Fork 所生成的 GC 性能概要图,包括目标利用率、堆内存使用和应用程序利用率:

图 11. Tuning Fork 性能概要

使用 Metronome 时的注意事项

Metronome 竭力为 GC 提供短暂确定的暂停,在应用程序代码中和底层平台中出现了一些可能影响这些结果的情形,有时会导致暂停时间异常。使用标准 JDK 收集器带来的 GC 行为更改在此也会出现。

RTSJ 规定 GC 不能处理永久内存。因为类位于永久内存中,所以它们不服从 GC 并且因此不能被卸载。希望使用大量类的应用程序需要适当地调整永久空间,要卸载类的应用程序需要在 WebSphere Real Time 中调整其编程模型。

Metronome 中的 GC 工作是基于时间的,而对硬件时钟的任何更改都可能导致难于诊断的问题。使系统时间与 Network Time Protocol (NTP) 服务器同步然后让硬件时钟和系统时间同步就是一个这样的例子。这将表现为时间上突然跳跃到 GC 并可能导致维持利用率目标的失败或者可能导致内存不足的错误。

在单个机器上运行多个 JVM 可能造成 JVM 之间相互干扰,使利用率图发生倾斜。alarm 线程是一个高优先级 RT 线程,它会抢占任何其他具有较低优先级的线程,而 GC 线程仍然以 RT 优先级运行。如果在任何时间都有足够的 GC 和 alarm 线程是活动的,则没有活动的 GC 循环的 JVM 可能使其应用程序线程被其他 JVM 的 GC 和 alarm 线程抢占,而时间实际上分配给了应用程序,原因是该 VM 的 GC 是不活动的。

学习不是人生的全部,但学习都征服不了,那我还能做什么?

实时Java,第4部分-实时垃圾收集

相关文章:

你感兴趣的文章:

标签云: