深入理解System.nanoTime()
  1. nanoTime
    1. nanoTime在Linux下的实现
    2. nanoTime在Windows下的实现
  2. 总结
  3. 备注

nanoTime

前几天分析了System.currentTimeMillis()的实现,今天我们来看看和它比较相似但又不同的System.nanoTime()System.nanoTime()的目的是用于测量时间的差值或者说时间的流逝

void func() {
  // do something
}

void test() {
  long begin = System.nanoTime();
  func();
  long end = System.nanoTime();
  long diff = end - begin;
}

注意,System.nanoTime()返回的并不是当前时间(和System.currentTimeMillis()的含义不一样),而是当前时刻一个固定但是可以任意的时间点的差值(是不是有点绕😜,为了表述的严谨只能如此),也就是说返回值可能是正数也可能是负数,但实际中的实现一般是当前时刻到过去的某个时间点(比如Linux用的系统启动时间点)的差值;所以只能用若干System.nanoTime()调用获取其差值做一些判断或运算,换而言之一次调用基本是没有意义的;而且不同Java虚拟机调用System.nanoTime()用的起始点可能是不一样的,也就是说不同虚拟机之间不能用其值来判断时间流逝。

接下来我们看看,System.nanoTime()在Java中的声明:

/**
     * Returns the current value of the running Java Virtual Machine's
     * high-resolution time source, in nanoseconds.
     *
     * This method can only be used to measure elapsed time and is
     * not related to any other notion of system or wall-clock time.
     * The value returned represents nanoseconds since some fixed but
     * arbitrary <i>origin</i> time (perhaps in the future, so values
     * may be negative).  The same origin is used by all invocations of
     * this method in an instance of a Java virtual machine; other
     * virtual machine instances are likely to use a different origin.
     *
     * <p>This method provides nanosecond precision, but not necessarily
     * nanosecond resolution (that is, how frequently the value changes)
     * - no guarantees are made except that the resolution is at least as
     * good as that of {@link #currentTimeMillis()}.
     *
     * <p>Differences in successive calls that span greater than
     * approximately 292 years (2<sup>63</sup> nanoseconds) will not
     * correctly compute elapsed time due to numerical overflow.
     *
     * <p>The values returned by this method become meaningful only when
     * the difference between two such values, obtained within the same
     * instance of a Java virtual machine, is computed.
     *
     * <p>For example, to measure how long some code takes to execute:
     * <pre> {@code
     * long startTime = System.nanoTime();
     * // ... the code being measured ...
     * long elapsedNanos = System.nanoTime() - startTime;}</pre>
     *
     * <p>To compare elapsed time against a timeout, use <pre> {@code
     * if (System.nanoTime() - startTime >= timeoutNanos) ...}</pre>
     * instead of <pre> {@code
     * if (System.nanoTime() >= startTime + timeoutNanos) ...}</pre>
     * because of the possibility of numerical overflow.
     *
     * @return the current value of the running Java Virtual Machine's
     *         high-resolution time source, in nanoseconds
     * @since 1.5
     */
    @HotSpotIntrinsicCandidate
    public static native long nanoTime();

由上述声明和注释我们可以知道,Java提供的System.nanoTime()返回以纳秒为单位的高精度时间,不过这并不意味着其准确度就是1纳秒。特别注意这这里:

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time.

JavaSystem.nanoTime()

System.currentTimeMillis()一样,System.nanoTime()在这里只有声明,而没有具体实现,其实现由更底层的C/C++完成。System.nanoTime()对应到实现源码在System.c中:

/* Only register the performance-critical methods */static JNINativeMethod methods[] = {
    {"currentTimeMillis", "()J",              (void *)&JVM_CurrentTimeMillis},
    {"nanoTime",          "()J",              (void *)&JVM_NanoTime},
    {"arraycopy",     "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy},
};

通过函数指针JVM_NanoTime找到其对应实现:

JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored))
  JVMWrapper("JVM_NanoTime");
  return os::javaTimeNanos();      
JVM_END

到这里我们发现,Java实现System.nanoTime()还是依赖不同OS的实现;接下来,我们就看看Linux和Window平台下的实现。

nanoTime在Linux下的实现

我们在os_linux.cpp文件中找到nanoTime的实现:

jlong os::javaTimeNanos() {
  // 先检查当前的OS是否支持monotonic clock
  if (os::supports_monotonic_clock()) {
    // 支持monotonic clock
    struct timespec tp;
    // 调用Linux平台下的clock_gettime()获取事件信息
    // 注意这里的传入类型是CLOCK_MONOTONIC
    int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
    assert(status == 0, "gettime error");
    // 对clock_gettime()返回的时间信息,进行单位转换后返回
    jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
    return result;
  } else {
    // 不支持monotonic clock,则调用gettimeofday()获取时间信息
    // 这部分代码和System.currentTimeMillis()在Linux平台实现
    // 完全一样
    timeval time;
    int status = gettimeofday(&time, NULL);
    assert(status != -1, "linux error");
    jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
    return 1000 * usecs;
  }
}

通过上述代码和注释,我们可以对javaTimeNanos()有一个大致的了解,其本身也不复杂,其基本流程如下图:

如果当前Linux并不支持monotonic clock,System.nanoTime()和System.currentTimeMillis()实际上就是等价的,但这种情况目前一般不会出现,可能会出现在一些非常古老的Linux版本上,并且如果当前系统不支持monotonic clock,Java也给出了警告:

// os_linux.cpp
void os::Linux::clock_init() {
  // we do dlopen's in this particular order due to bug in linux
  // dynamical loader (see 6348968) leading to crash on exit
  void* handle = dlopen("librt.so.1", RTLD_LAZY);
  if (handle == NULL) {
    handle = dlopen("librt.so", RTLD_LAZY);
  }

  if (handle) {
    int (*clock_getres_func)(clockid_t, struct timespec*) =
           (int(*)(clockid_t, struct timespec*))dlsym(handle, "clock_getres");
    int (*clock_gettime_func)(clockid_t, struct timespec*) =
           (int(*)(clockid_t, struct timespec*))dlsym(handle, "clock_gettime");
    if (clock_getres_func && clock_gettime_func) {
      // See if monotonic clock is supported by the kernel. Note that some
      // early implementations simply return kernel jiffies (updated every
      // 1/100 or 1/1000 second). It would be bad to use such a low res clock
      // for nano time (though the monotonic property is still nice to have).
      // It's fixed in newer kernels, however clock_getres() still returns
      // 1/HZ. We check if clock_getres() works, but will ignore its reported
      // resolution for now. Hopefully as people move to new kernels, this
      // won't be a problem.
      struct timespec res;
      struct timespec tp;
      if (clock_getres_func (CLOCK_MONOTONIC, &res) == 0 &&
          clock_gettime_func(CLOCK_MONOTONIC, &tp)  == 0) {
        // yes, monotonic clock is supported
        _clock_gettime = clock_gettime_func;
        return;
      } else {
        // close librt if there is no monotonic clock
        dlclose(handle);
      }
    }
  }
  warning("No monotonic clock was available - timed services may " \
          "be adversely affected if the time-of-day clock changes");
}

在初始化时,会检测Linux内核是否支持clock_getresclock_gettime,如果不支持,Java会给出警告:

No monotonic clock was available - timed services may be adversely affected if the time-of-day clock changes.

os_linux.cpplock_init()

关于clock_getresclock_gettime的介绍请戳这里,特别注意里面关于时间类型CLOCK_MONOTONIC的描述:

A nonsettable system-wide clock that represents monotonic time since—as described by POSIX—“some unspecified point in the past”. On Linux, that point corresponds to the number of seconds that the system has been running since it was booted.

The CLOCK_MONOTONIC clock is not affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the clock), but is affected by the incremental adjustments performed by adjtime(3) and NTP. This clock does not count time that the system is suspended. All CLOCK_MONOTONIC variants guarantee that the time returned by consecutive calls will not go backwards, but successive calls may—depending on the architecture—return identical (not-increased) time values.

由上述可知,以类型ClOCK_MONOTONIC获取时间信息时:

  • 在Linux下和系统的启动时间点相关;
  • 手动修改本地时间,并不会影响clock_gettime()获取的clock;但是会受到adjtime和NTP的影响;
  • 并不会计算系统暂停的时间流逝。

nanoTime在Windows下的实现

在os_windows.cpp中找到javaTimeNanos()的实现:

jlong os::javaTimeNanos() {
    LARGE_INTEGER current_count;
  	// Windows提供的函数:查询当前的计数
    QueryPerformanceCounter(&current_count);
    double current = as_long(current_count);
    // 获取频率
    double freq = performance_frequency;
  	// current除以freq才是对应的时间信息
    // 将时间信息转换成纳秒单位
    jlong time = (jlong)((current/freq) * NANOSECS_PER_SEC);
    return time;
}

由上述代码,我们推测Windows获取高精度的时间信息是通过一个单调递增的计数器,计数器的增长就意味着时间的流逝;那么计数器的增长必然是有一个频率的,也就是说多长时间执行一次计数器增长操作。接下来我们就看看:

// os_windows.cpp
static jlong initial_performance_count;
static jlong performance_frequency;
void os::win32::initialize_performance_counter() {
  LARGE_INTEGER count;
  // Windows提供的函数:查询更新计数的频率
  QueryPerformanceFrequency(&count);
  performance_frequency = as_long(count);
  // 查询当前的计数
  QueryPerformanceCounter(&count);
  // 记录起始的计数
  initial_performance_count = as_long(count);
}
// 初始化系统信息
void os::win32::initialize_system_info() {
  /**
   * 。。。 省略
   */
  initialize_performance_counter();
}

上述初始化操作,印证了我们的推测;这也意味着在Windows平台下QueryPerformanceFrequency()返回的信息——这个频率决定了我们获取时间的准确度。

Windows关于QueryPerformanceFrequency()的解释:

Retrieves the frequency of the performance counter. The frequency of the performance counter is fixed at system boot and is consistent across all processors. Therefore, the frequency need only be queried upon application initialization, and the result can be cached.

Windows关于QueryPerformanceCounter()的解释:

Retrieves the current value of the performance counter, which is a high resolution (<1us) time stamp that can be used for time-interval measurements.

根据上述信息,我们可以知道在Windows平台下,这个高精度时间信息同样和系统启动时间相关。

总结

通过深入理解System.nanoTime()在Linux平台和Windows平台的不同实现代码,我们可以发现不论是Linux还是Windows都是通过系统启动时间点来获取我们需要的高精度单调递增的时间信息。通过System.nanoTime()我们获取了以纳秒为单位的高精度时间信息(记住,其准确度不一定是1纳秒),但同时我们也会付出更大的性能代价(比起System.currentTimeMillis()来说)。最后,也有其他方式获取精度更高的时间信息(CPU时钟),当然性能代价更大,这就可能需要依赖具体某个平台的实现了,这里只关心Java源码的实现,就不继续深入探讨了。

备注

0.以上所说java的版本均为java11,实现java本身的源码来自OpenJdk 11。

1.OS是operater system,即操作系统的缩写。

2.System.c位于Java源码目录下的java.base/share/native/libjava/System.c。

3.探究java方法的实现,目的是理解其原理做到知其然知其所以然,但并不会陷入过多的实现细节,比如c/C++里的宏定义等。

4.NTP指Network Time Protocol,即网络时间协议。