为什么System.nanoTime()和System.currentTimeMillis()会快速偏离?

23
为了诊断目的,我希望能够检测长时间运行的服务器应用程序中系统时钟的更改。由于System.currentTimeMillis()基于挂钟时间,而System.nanoTime()基于独立(*)于挂钟时间的系统计时器,我认为可以使用这些值之间的差异变化来检测系统时钟的更改。
我编写了一个快速测试应用程序,以查看这些值之间的差异稳定性,但令我惊讶的是,这些值立即在每秒几毫秒的级别上发散。有几次我看到了更快的分歧。这是在一个安装有Java 6的Win7 64位桌面上进行的。我还没有尝试在Linux(或Solaris或MacOS)下运行此测试程序以查看其表现如何。对于一些运行该应用程序的情况,发散是正的,对于一些运行,它是负的。这似乎取决于桌面正在做什么,但很难说。
public class TimeTest {
  private static final int ONE_MILLION  = 1000000;
  private static final int HALF_MILLION =  499999;

  public static void main(String[] args) {
    long start = System.nanoTime();
    long base = System.currentTimeMillis() - (start / ONE_MILLION);

    while (true) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        // Don't care if we're interrupted
      }
      long now = System.nanoTime();
      long drift = System.currentTimeMillis() - (now / ONE_MILLION) - base;
      long interval = (now - start + HALF_MILLION) / ONE_MILLION;
      System.out.println("Clock drift " + drift + " ms after " + interval
                         + " ms = " + (drift * 1000 / interval) + " ms/s");
    }
  }
}
< p > Thread.sleep() 的时间不准确,以及中断,应该与计时器漂移完全无关。

这两个Java "System"调用旨在用作测量 - 一个用于测量壁钟时间的差异,另一个用于测量绝对间隔,因此当实时时钟未被更改时,这些值应该以非常接近相同的速度变化,对吗?这是Java中的错误、弱点还是失败?有没有什么OS或硬件阻止Java更加准确?

我完全期望这些独立测量之间存在一定的漂移和抖动(**),但我预计每天的漂移应该少于一分钟。如果单调地漂移1毫秒/秒,那么将近90秒!我观察到的最坏情况漂移可能是那个的十倍。每次运行此程序时,我都会看到第一个测量的漂移。到目前为止,我还没有运行程序超过30分钟。

我希望看到打印值中的一些小随机性,由于抖动,但在几乎所有运行程序的情况下,我看到差异的稳定增加,通常每秒增加3毫秒,有时更多。

任何Windows版本是否具有类似于Linux的机制,调整系统时钟速度,缓慢将日历时钟与外部时钟源同步?这种事情会影响两个计时器还是只有壁钟计时器?

(*) 我了解到,在某些体系结构上,System.nanoTime() 必须使用与 System.currentTimeMillis() 相同的机制。我也相信可以假设任何现代Windows服务器都不是这样的硬件体系结构。这是一个错误的假设吗?

(**) 当然,System.currentTimeMillis() 在大多数系统上其粒度不是1毫秒,因此通常具有更大的抖动比 System.nanoTime()


GetSystemTimeAdjustment()函数将返回有关调整是否处于活动状态以及其参数设置的信息。 - Arno
1
哈!你觉得你有问题吗?!我曾经看到过漂移高达0.01,在一个小时内,也就是在System.nanoTime()增加了3600 000 000 000后,只过去了59分半钟! - Feuermurmel
5个回答

14

你可能会对这篇 Sun/Oracle 的关于 JVM 定时器的博客文章感兴趣:(链接)

以下是这篇文章中有关 Windows 下 JVM 定时器的内容:

System.currentTimeMillis() 是使用 GetSystemTimeAsFileTime 方法实现的,该方法实际上只读取 Windows 维护的低分辨率时间值。根据报告的信息,读取此全局变量自然非常快 - 大约为 6 个周期。无论计时器中断如何编程,这个时间值以恒定速率更新 - 根据平台,这将是 10 毫秒或 15 毫秒(此值似乎与默认中断周期相关)。

System.nanoTime() 是使用 QueryPerformanceCounter / QueryPerformanceFrequency API 实现的(如果可用,否则返回 currentTimeMillis*10^6)。QueryPerformanceCounter(QPC)根据其运行的硬件以不同的方式实现。通常它会使用可编程间隔计时器 (PIT)、ACPI 电源管理计时器 (PMT) 或 CPU 级时间戳计数器 (TSC)。访问 PIT/PMT 需要执行缓慢的 I/O 端口指令,因此 QPC 的执行时间大约为微秒级别。相比之下,读取 TSC 大约需要 100 个时钟周期(从芯片中读取 TSC 并根据操作频率将其转换为时间值)。如果你的系统使用 ACPI PMT,则可以通过检查 QueryPerformanceFrequency 是否返回签名值 3,579,545(即 3.57MHz)来判断。如果你看到的值约为 1.19MHz,则表示你的系统正在使用旧的 8245 PIT 芯片。否则,你应该看到一个大约等于你 CPU 频率的值(模块任何可能生效的速度限制或电源管理)。


1
我已经找到了相同的博客文章。虽然这并没有完全回答我的问题,但我认为这已经是最接近答案的了。 - Eddie
Sun曾经记录了一个bug,关于System.nanoTime()的问题,直到2015年才被标记为已修复。该问题是针对Linux系统提出的,但在评论中指出它也影响Windows系统。此外,本答案的文本摘自2006年的一篇博客文章,但在随后的几年中,该文章在几个关键的开发报告中被驳斥。有意义的部分是声明:“根据其运行的硬件实现方式不同。” - ingyhere
在错误报告中声称只影响Java 5和6。我需要看到更多的修复证据,才能满意地确认解决方案。(它是否仅因原始报告的日期而说它影响Java 5/6?)无论如何,这似乎非常依赖于实现,并且受系统之间的不一致性的影响。 - ingyhere

8
我不确定这会有多大帮助。但在Windows/Intel/AMD/Java世界中,这是一个活跃变化的领域。准确和精确的时间测量需求已经显而易见了至少十年。Intel和AMD都通过改变TSC的工作方式做出了回应。两家公司现在都拥有称为Invariant-TSC和/或Constant-TSC的东西。

查看rdtsc accuracy across CPU cores。引用osgx(提到Intel手册)的话:

"16.11.1 不变TSC

新处理器中的时间戳计数器可能支持一种增强功能,称为不变TSC。处理器对不变TSC的支持由PUID.80000007H:EDX[8]表示。

不变TSC将在所有ACPI P、C和T状态下以恒定速率运行。这是未来的架构行为。在支持不变TSC的处理器上,操作系统可以使用TSC作为墙上时钟计时器服务(而不是ACPI或HPET计时器)。TSC读取效率更高,不会产生环路转换或访问平台资源所带来的开销。

另请参见http://www.citihub.com/requesting-timestamp-in-applications/。引用作者的话:

对于AMD:

如果CPUID 8000_0007.edx[8]=1,则确保TSC速率在所有P状态、C状态和停止授权转换(如STPCLK节流)中是不变的;因此,TSC适用于用作时间源。

对于Intel:

CPUID.80000007H:EDX[8]指示了处理器对不变TSC的支持。在所有ACPI P、C和T状态下,不变TSC将以恒定速率运行。这是未来的架构行为。在支持不变TSC的处理器上,操作系统可以使用TSC作为墙钟计时器服务(而不是ACPI或HPET计时器)。TSC读取更有效率,并且不会产生与环路转换或访问平台资源相关的开销。

现在真正重要的是,最新的JVM似乎利用了新的可靠TSC机制。网上没有太多关于此的信息。但是,请查看http://code.google.com/p/disruptor/wiki/PerformanceResults

"为了衡量延迟,我们采用三级流水线,在不饱和的情况下生成事件。这是通过在注入事件后等待1微秒再注入下一个,并重复5000万次来实现的。要在这个精度级别上计时,需要使用CPU的时间戳计数器。我们选择具有不变TSC的CPU,因为旧处理器由于省电和睡眠状态而频率变化。英特尔Nehalem及更高版本的处理器使用不变TSC,可以在运行Ubuntu 11.04的最新Oracle JVM上访问。此测试未使用CPU绑定"
"请注意,“Disruptor”的作者与Azul和其他JVM的开发人员有密切联系。"
"另请参见“Java Flight Records Behind the Scenes”。此演示文稿提到了新的不变TSC指令。"

2
有没有任何一个版本的Windows有类似于Linux的机制,可以调整系统时钟速度,使时间日历时钟与外部时钟源缓慢同步?这样的东西会影响两个计时器还是只影响墙上的时钟计时器? Windows Timestamp Project可以做到你所要求的。据我所知,它只影响墙上的时钟计时器。

2

返回可用的最精确系统计时器的当前值,以纳秒为单位。

此方法仅用于测量经过的时间,与任何其他系统或挂钟时间概念无关。返回的值表示自某个固定但任意时间(可能在未来,因此值可能为负)以来的纳秒数。此方法提供纳秒精度,但不一定具有纳秒精度。不保证值更改的频率。跨越大约292年(2 ** 63纳秒)的连续调用之间的差异由于数字溢出而不能准确计算经过的时间。

请注意,它说“精确”,而不是“准确”。

这不是Java中的“错误”或任何东西中的“错误”。这是一个定义。JVM开发人员查找系统中最快的时钟/计时器并使用它。如果与系统时钟同步,那么很好,但如果不是,那就是饼干碎裂的方式。完全有可能,例如,计算机系统将具有准确的系统时钟,但然后具有内部高速率计时器,该计时器与CPU时钟率或类似物相结合。由于时钟速率通常变化以最小化功耗,因此此内部计时器的增量率会变化。


2
JavaDoc中说:“返回的值表示自某个固定但任意时间以来的纳秒数”,这告诉我该值的增量速率不应随时间(大幅)变化。如果您的CPU时钟速率下降了一半,计数器的增量速率也下降了一半,那么您就不能很好地使用它来测量经过的时间,对吗?按照您描述的方式,它除了是否正在流逝时间之外,没有其他可靠的测量对象。 - Eddie

2

System.currentTimeMillis()System.nanoTime()不一定由同一硬件提供支持。System.currentTimeMillis()GetSystemTimeAsFileTime()支持,具有100ns分辨率元素。它的源是系统计时器。而System.nanoTime()则由系统的高性能计数器支持。提供此计数器的硬件种类繁多,因此其分辨率因底层硬件而异。

在任何情况下,都不能假设这两个来源是同步的。将这两个值相互比较会显示出不同的运行速度。如果将System.currentTimeMillis()的更新视为时间的实际进展,则System.nanoTime()的输出有时可能更慢,有时可能更快,并且也会变化。

必须进行仔细的校准才能使这两个时间源保持同步。

关于这两个时间源之间关系的更详细描述可在Windows Timestamp Project中找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接