Creating a Basic Stopwatch with Java Time Classes
Measuring elapsed time is a common need in software. A stopwatch here means recording when something starts, when it stops, and how much time has passed in between. Java gives more than one option for this. The older method is System.currentTimeMillis, which returns the current time in milliseconds counted from the Unix epoch. A newer and more flexible option is found in the java.time package, where classes like Instant represents a specific moment with nanosecond resolution. Both choices depend on mechanics managed by the runtime and the operating system, and the differences show how clocks are read and stored behind the scenes.
I publish free articles like this daily, if you want to support my work and get access to exclusive content and weekly recaps, consider subscribing to my Substack.
Mechanics of Tracking Elapsed Time
Measuring time inside software comes down to picking a reliable source, recording two readings, and comparing them. At first glance it looks like nothing more than subtraction, but the way Java provides access to different clocks makes it worth a closer look. Each method has its own strengths, and the choice affects how precise your stopwatch can be and how it reacts to changes in the system clock.
The Flow of a Stopwatch
A stopwatch can be broken into three simple actions. You press start, some work takes place, and then you press stop. The difference between those two readings gives the elapsed duration. In code, this usually means calling a clock method at the beginning, another at the end, and subtracting the first from the second.
public class StopwatchFlow {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(500);
long stop = System.currentTimeMillis();
long elapsed = stop - start;
System.out.println("Elapsed ms: " + elapsed);
}
}This uses milliseconds as the unit of measurement, which is often enough for everyday code but doesn’t provide the highest resolution.
How System.currentTimeMillis Works
The method System.currentTimeMillis reports the number of milliseconds since midnight on January 1, 1970 UTC. That value is called epoch time, and it’s stored in a long. Each time you call it, the JVM asks the operating system for the current wall clock reading, which is backed by hardware timers on the machine.
One important detail is that this reading can move forward and backward if the system clock changes. Adjustments from network time synchronization, daylight saving transitions, or manual changes by a user all affect the result. That means you can measure elapsed time with it, but you’re also at the mercy of whatever shifts happen to the system clock.
public class CurrentTimeExample {
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
long time2 = System.currentTimeMillis();
System.out.println("First reading: " + time1);
System.out.println("Second reading: " + time2);
}
}Running this prints two numbers representing the current epoch time in milliseconds. Subtracting the first from the second gives you the elapsed span. For longer measurements, such as timing minutes or hours, this method works well enough, but for high precision it can fall short.
Another way developers use this method is to stamp log entries. A timestamp in milliseconds is cheap to grab and gives a quick way to order events in a file or output stream.
How Instant Works
The Instant class, introduced in Java 8, represents a specific point in time with nanosecond resolution. Internally, it keeps track of two values: one for seconds since the epoch and one for the fractional part of a second. Together they create a precise view of the timeline that integrates with other parts of the java.time package.
When you call Instant.now(), the JVM again consults the operating system. The result is then wrapped into an Instant object. With that object you can perform operations like adding durations, comparing two instants, or converting to different representations such as ZonedDateTime.
import java.time.Instant;
import java.time.Duration;
public class InstantExample {
public static void main(String[] args) throws InterruptedException {
Instant start = Instant.now();
Thread.sleep(300);
Instant stop = Instant.now();
Duration gap = Duration.between(start, stop);
System.out.println("Elapsed nanoseconds: " + gap.toNanos());
}
}This code returns a Duration that you can query in nanoseconds, subject to the precision the operating system provides. The granularity depends on the hardware clock, so not all environments reach full nanosecond resolution, but the class can represent that level of detail if it’s available.
Get Alexander Obregon’s stories in your inbox
Join Medium for free to get updates from this writer.
You can also take advantage of the fact that Instant is an object. It can be passed around, stored in collections, or formatted as a string with ease. That flexibility makes it well suited for systems where you need to store and manipulate time data beyond simple measurements.
import java.time.Instant;
public class InstantFormatting {
public static void main(String[] args) {
Instant now = Instant.now();
System.out.println("Current moment: " + now.toString());
}
}This code prints the instant in ISO-8601 format, which is widely used for timestamps across systems and databases.
Monotonic Sources and Accuracy
Both System.currentTimeMillis and Instant.now() depend on the system clock, which is not monotonic. A monotonic clock is one that always moves forward without ever jumping backward. For measurements where precision matters and clock adjustments could introduce errors, Java provides System.nanoTime.
nanoTime gives a reading in nanoseconds relative to some fixed but arbitrary point, not tied to the epoch. That makes it useless for timestamps but ideal for elapsed measurements, because the only thing that matters is the difference between two calls.
public class NanoTimeExample {
public static void main(String[] args) throws InterruptedException {
long start = System.nanoTime();
Thread.sleep(700);
long stop = System.nanoTime();
long gap = stop - start;
System.out.println("Elapsed ms: " + gap / 1_000_000.0);
}
}This one measures a pause and reports the result in milliseconds, converted from nanoseconds. Because the source is monotonic, no system adjustment will affect the outcome, which makes it better suited for timing short blocks of code or performance tests.
Memory and Execution
When you capture time with System.currentTimeMillis or System.nanoTime, the JVM stores the result as a primitive long. That value lives in the stack frame of the method where you assigned it. The operation is fast, as it only requires a native call to the host operating system and a copy into a variable.
With Instant, things are slightly different. An Instant object holds both seconds and nanoseconds, the local variable is a reference, and the JVM typically allocates the object, though the JIT can stack-allocate or even remove it through escape analysis. Garbage collection manages its lifecycle, but the Just-In-Time compiler can optimize away the allocation if it detects that the object doesn’t escape the current method. That means under certain conditions an Instant is as cheap to use as a primitive, even though it provides more features.
The following snippet shows how an object reference is handled at the code level:
import java.time.Instant;
public class InstantMemory {
public static void main(String[] args) {
Instant point = Instant.now();
System.out.println("Stored reference: " + point);
}
}While the reference is on the stack, the actual data is managed on the heap. Modern JVMs optimize this so well that the overhead is minimal in practice, which is why Instant has become the preferred choice in many modern systems.
Conclusion
Creating a stopwatch in Java depends on how the runtime connects with different clocks provided by the operating system. System.currentTimeMillis gives you millisecond readings based on the system clock, while Instant offers finer resolution and richer time handling. When accuracy must not be affected by clock shifts, System.nanoTime provides a monotonic source that always moves forward, making it better for short measurements. Underneath, values are kept either as primitives on the stack or as objects on the heap, with the JIT trimming away overhead whenever possible. What feels like a simple start and stop routine is backed by detailed mechanics that govern how time is captured and processed inside the JVM.
- System.currentTimeMillis Documentation
- Instant Class Documentation
- Duration Class Documentation
- System.nanoTime Documentation
Thanks for reading! If you found this helpful, highlighting, clapping, or leaving a comment really helps me out.







