프로그래밍/Java

[Java] JDK 부수기 - (2) java.lang.System - 5. currentTimeMillis, nanoTime

Churnobyl 2023. 11. 29. 18:24
728x90
반응형

 


5. currentTimeMillis, nanoTime

각각 OS로부터 밀리초, 나노초 단위로 시간을 받아온다.

 

JVM 뜯어보기

 System.currentTimeMillis(), System.nanoTime()은 System클래스에서 시간을 다루는 두 메서드지만 전자는 주로 현재 시간을 나타내거나 시간 차이를 비교할 때 사용하지만, 후자는 시간 차이를 정밀하게 비교할 때 사용한다. 두 메서드의 작동 방식에 차이가 있기 때문인데 JVM을 뜯어보면서 함께 비교해보자. System 클래스에서 두 메서드는 다음과 같이 코딩되어 있다.

 

// src/java.base/java/lang/System.java

public final class System {

    @IntrinsicCandidate
    public static native long currentTimeMillis();
    
    @IntrinsicCandidate
    public static native long nanoTime();
    
}

 

 두 메서드 모두 네이티브 메서드로 구현되어 있다. 그럼 매핑된 두 메서드를 확인해보자.

 

// src/hotspot/share/prims/jvm.cpp

JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored))
  return os::javaTimeMillis();
JVM_END

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

 

 두 메서드는 각각 JVM_CurrentTimeMillis, JVM_NanoTime 함수로 매핑되어 있으며 각각 JVM의 os 클래스에서 운영체제에 맞는 javaTimeMillis()javaTimeNanos() 함수를 호출해 리턴한다. 필자의 OS는 Windows이므로 Windows로 각각의 함수를 살펴보자.

 

// src/hotspot/os/windows/os_windows.cpp

jlong os::javaTimeMillis() {
  FILETIME wt;
  GetSystemTimeAsFileTime(&wt);
  return windows_to_java_time(wt);
}

jlong os::javaTimeNanos() {
    LARGE_INTEGER current_count;
    QueryPerformanceCounter(&current_count);
    double current = current_count.QuadPart;
    jlong time = (jlong)(current * nanos_per_count);
    return time;
}

 

 여기서부터 두 함수의 동작 방식에 차이가 난다.

 

javaTimeMillis

 먼저 javaTimeMillis() 함수는 Windows API에서 날짜와 시간을 나타내는데 사용되는 FILETIME 구조체 wt를 선언한다. 참고로 FILETIME 구조체는 상위 32bit 비트를 나타내는 dwHighDateTime, 하위 비트 32bit를 나타내는 dwLowDateTime 총 64bit로 구성되어 있으며 100나노초 단위의 시간 간격을 나타낸다. 이를 다시 Windows API의 GetSystemTimeAsFileTime() 함수를 이용해 시스템 시간을 wt로 복사한다. 최종적으로 windows_to_java_time() 함수로 Windows API의 FILETIME 형식을 Java가 이해할 수 있는 형식으로 변환한 뒤 리턴한다. windows_to_java_time() 함수를 살펴보자.

 

// src/hotspot/os/windows/os_windows.cpp

static jlong  _offset   = 116444736000000000;

jlong offset() {
  return _offset;
}

jlong windows_to_java_time(FILETIME wt) {
  jlong a = jlong_from(wt.dwHighDateTime, wt.dwLowDateTime);
  return (a - offset()) / 10000;
}

 

 windows_to_java_time() 함수는 다음과 같다. jlong_from() 함수로 wt의 상위 32bit, 하위 32bit를 합쳐 jlong타입 64bit로 변환해 a로 저장한다. 그 다음엔 (a - offset()) / 10000을 리턴하는데 그 이유는 다음과 같다. FIMETIME 형식은 1601년 1월 1일 0시 UTC부터 100나노초 단위로 계산하는데 반해 Java는 Unix시간을 사용하므로 1970년 1월 1일 0시 UTC부터 초 단위로 시간을 측정한다. 이러한 기준 날짜 차이를 보정하기 위한 offset값이 _offset값이며, 100나노초를 1밀리초 단위로 환산하기 위한 값이 10000이다. 결론적으로 System.currentTimeMillis() 메서드는 100나노초 단위의 시스템 시간 값을 1밀리초 단위의 Unix시간으로 환산해서 출력하는 메서드다.

 

javaTimeNanos

 다음으로 javaTimeNanos() 함수는 FILETIME 구조체가 아니라 LARGE_INTEGER 구조체 current_count를 선언한다. 그리고 Windows API의 QueryPerformanceCounter() 함수를 이용해 시스템이 시작하고 난 후로 고성능 타이머가 카운트한 총 틱 수를 current_count에 저장하도록 한다. 다음으로 current_count의 QuadPart멤버를 가져와  64bit 정수값을 current에 할당한다. 이제 current와 nanos_per_count(카운트 당 나노초)를 곱한 값을 jlong타입 time에 저장하고 리턴한다. 즉 시스템이 시작한 이후 고성능 타이머가 카운트한 값을 나노초로 변환한 값을 리턴하는 것System.nanoTime()이다. 중요한 것은 기준이 시스템 시작 시간이다.

 

 참고로 nanos_per_count는 다음과 같이 계산된다.

 

// src/hotspot/os/windows/os_windows.cpp

static jlong initial_performance_count;
static jlong performance_frequency;
static double nanos_per_count; // NANOSECS_PER_SEC / performance_frequency

void os::win32::initialize_performance_counter() {
  LARGE_INTEGER count;
  QueryPerformanceFrequency(&count);
  performance_frequency = count.QuadPart;
  nanos_per_count = NANOSECS_PER_SEC / (double)performance_frequency;
  QueryPerformanceCounter(&count);
  initial_performance_count = count.QuadPart;
}

 

// src/hotspot/share/utilities/globalDefinitions.hpp

const jlong NANOSECS_PER_SEC      = CONST64(1000000000);

 

 nanos_per_count는 initialize_performance_counter() 함수에서 계산된다. 해당 함수는 JVM이 실행되면서 스레드가 실행될 때 함께 실행된다. 간단히 살펴보면 QueryPerformanceFrequency() 함수를 통해 1초 당 고성능 타이머가 카운트하는 틱 수count에 저장하고, count의 QuadPart 멤버를 가져와서 64bit 정수값을 performance_frequency에 저장한다. 그리고 그 값으로 상수 NANOSECS_PER_SEC을 나눈 값이 nanos_per_count가 된다. 말이 늘어져서 조금 어렵게 느껴질 수도 있지만 1초를 나노초로 환산한 1,000,000,000나노초를 1초 동안 타이머가 카운트한 틱 수로 나눠서 1틱 당 몇 나노초가 걸리는지 구한 것이다. 그럼 위의 javaTimeNanos() 함수에서 current * nanos_per_count가 시스템 시작 후 현재까지의 나노초를 구한 결과라는 것을 이해할 수 있다.

 

 여담이지만 참고로 Windows 10 build 1809부터 QueryPerformanceFrequency() 함수는 고정적으로 10,000,000를 반환한다. 이는 운영체제 차원에서 보정하도록 한 것 같다. 결과적으로 해당 버전 이상의 Windows 운영체제에서는 nanos_per_count는 그냥 100, 즉 100나노초다. 궁금해서 다음과 같이 C#으로 결과를 만들어보았다. 참고

 

using System;
using System.Runtime.InteropServices;

internal class Program
{
    [DllImport("Kernel32.dll")]
    static extern bool QueryPerformanceFrequency(out long lpFrequency);

    [DllImport("Kernel32.dll")]
    static extern bool QueryPerformanceCounter(out long lpCounter);

    static void Main(string[] args)
    {
        long lpFrequency;
        long lpCounter;
        QueryPerformanceFrequency(out lpFrequency);
        QueryPerformanceCounter(out lpCounter);
        Console.WriteLine($"QueryPerformanceFrequency == {lpFrequency}");
        Console.WriteLine($"QueryPerformanceCounter == {lpCounter}");
    }
}
QueryPerformanceFrequency == 10000000
QueryPerformanceCounter ==1153764244693

 

 다음과 같이 10,000,000가 딱 나오는 것을 알 수 있다. QueryPerformanceCounter의 결과는 시스템이 시작한 후 몇 틱이나 지났느냐니까 1153764244693에 100을 곱해 나노초 단위로 바꿔주고 시간으로 환산하면 32.049006797028시간이 나온다.

 

작업 관리자

 

 작업관리자에서 CPU 작동 시간을 봐도 1일 + 8시간 = 32시간으로 딱 맞는 것을 볼 수 있다.

 

사용해보기

 자바에서 System.currentTimeMillis(), System.nanoTime()을 이용한 간단한 예제를 만들어보자.

 

import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    public static void main(String[] args) throws InterruptedException {

        long currentTimeMillis1 = System.currentTimeMillis();
        long nanoTime1 = System.nanoTime();

        Thread.sleep(20); // 20밀리초 간 pause

        long currentTimeMillis2 = System.currentTimeMillis();
        long nanoTime2 = System.nanoTime();

        System.out.println(currentTimeMillis2 - currentTimeMillis1);
        System.out.println(nanoTime2 - nanoTime1);

        System.out.println("========================");

        long currentTimeMillis3 = System.currentTimeMillis();
        System.out.println(currentTimeMillis3);
        Date date = new Date(currentTimeMillis3);

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(date.toString());
        System.out.println(formatter.format(date));
    }
}
20
20331600
========================
1701249123419
Wed Nov 29 18:12:03 KST 2023
2023-11-29 18:12:03

 

 크게 두가지 출력 결과를 만들었다. 첫번째는 스레드에 20밀리초 간 sleep을 준 뒤, 시간 비교를 하는 예제다. 결과는 다음과 같이 currentTimeMillis() 메서드의 시간 차이는 20(밀리초), nanoTime() 메서드의 시간 차이는 20331600(나노초)로 나왔다. Windows 10 build 1809 이상에서는 항상 1틱 당 100나노초로 고정되어 있으므로 자바에서 nanoTime()을 아무리 사용해도 100나노초 미만의 정확도는 나올 수 없다.

 

 두번째로는 currentTimeMillis() 메서드를 이용해 현재 시간을 출력하는 예제다. currentTimeMillis() 메서드의 결과인 currentTimeMillis3을 그대로 출력하면 다음과 같이 1970년 1월 1일 0시 UTC를 0 밀리초로 기준 삼고 현재를 밀리초로 계산한 결과를 출력한다. 이제 Date 객체에 이 값을 집어넣고 toString() 메서드로 출력한 결과와 SimpleDateFormat 클래스의 format() 메서드로 출력한 결과는 다음과 같다.

반응형