~ / topics / Languages
$man lang-java-and-jvm

Java and the JVM

In brief: Java itself is not affected by the Year 2038 problem. It counts time in milliseconds stored in a 64-bit number, which lasts for millions of years. The problems come from Java code that turns those values into 32-bit int seconds, from data formats and databases that keep only 32 bits, and from the JVM's native code on 32-bit Linux. The java.time API, available since Java 8, is the safe choice.

Risk level Low for the platform. Medium for applications (via int casts, legacy schemas and 32-bit JVMs)
Status Core APIs are 64-bit. A known OpenJDK bug on 32-bit platforms (JDK-8279444) is open as of 2026-10-21
Last reviewed 2026-10-21

2038 status at a glance

Not affected or different deadline (1), and why

Overview

From the start, Java has represented the current time as a long count of milliseconds since 1970-01-01T00:00:00Z. System.currentTimeMillis() returns a long [1], java.util.Date wraps a long, and java.time.Instant stores a long of epoch seconds plus an int of nanoseconds. That covers -1000000000-01-01T00:00Z to 1000000000-12-31T23:59:59.999999999Z [2]. A signed 64-bit millisecond counter overflows only after about 292 million years, so the platform's own clock has no 2038 limit.

Java programs still fail in 2038 for three reasons:

  1. Developers convert to seconds and store the result in an int. The bug is common in caches, tokens, session stores and protocol code. Keycloak, for example, deprecated its int-returning Time.currentTime() "to avoid integer overflow beyond year 2038" [3].
  2. Interop with 32-bit fields: database TIMESTAMP columns, binary protocols, int columns in legacy schemas, and JNI code.
  3. The JVM's native layer on 32-bit operating systems still calls C functions that use the platform's 32-bit time_t. See Lang-C-and-CPP.

Technical details

The int seconds pitfall

// BUG: silently wraps on 2038-01-19T03:14:08Z
int expires = (int) (System.currentTimeMillis() / 1000) + ttl;

// What happens after the boundary:
long ms  = Instant.parse("2038-01-19T03:14:08Z").toEpochMilli();
int bad  = (int) (ms / 1000);
System.out.println(Instant.ofEpochSecond(bad));   // 1901-12-13T20:45:52Z

The (int) cast in Java never throws. It silently keeps the low 32 bits. Two fixes:

// FIX 1: keep long (or Instant) end to end
long expires = Instant.now().getEpochSecond() + ttl;

// FIX 2: if a 32-bit field truly can't change, fail loudly instead of wrapping
int wire = Math.toIntExact(epochSeconds);   // throws ArithmeticException: integer overflow

Both snippets were run on OpenJDK 21 on 2026-10-21. The cast printed 1901-12-13T20:45:52Z, and Math.toIntExact threw ArithmeticException: integer overflow.

Look for these patterns:

java.time is the fix

Instant, OffsetDateTime, ZonedDateTime and Duration are all 64-bit based. Instant.getEpochSecond() returns a long. Instant.toEpochMilli() throws ArithmeticException instead of wrapping when a value doesn't fit [2]. Prefer Instant in APIs. Convert to primitive numbers only at the edge, using a long.

JNI

jint is "signed 32 bits" and jlong is "signed 64 bits" [6]. Native methods that take or return a time should use jlong. On the C side, check the width of the platform time_t before converting:

/* BUG: jint truncates; time_t may be 32-bit on armhf/i386 */
JNIEXPORT jint JNICALL Java_Foo_now(JNIEnv *e, jclass c) { return (jint) time(NULL); }

/* FIX */
JNIEXPORT jlong JNICALL Java_Foo_now(JNIEnv *e, jclass c) { return (jlong) time(NULL); }

The JVM on 32-bit platforms

So a 32-bit JVM is only as 2038-safe as the C library and the build flags it was compiled with. Prefer 64-bit JVMs. On 32-bit Arm, use a distribution build made with 64-bit time_t.

Kotlin and Scala

Serialization formats used from the JVM

Format Time encoding 2038-safe?
Java serialization of Date / Instant long millis / long seconds + int nanos Yes
Protocol Buffers google.protobuf.Timestamp int64 seconds + int32 nanos, valid 0001–9999 [8] Yes
Apache Avro timestamp-millis / timestamp-micros annotate an Avro long [7] Yes
Avro date int days since epoch [7] Yes (int days lasts ~5.8 million years)
MessagePack timestamp 32 unsigned 32-bit seconds, range to 2106-02-07 [9] Until 2106. Libraries switch to timestamp 64/96 automatically when needed
Hand-rolled writeInt(seconds) signed 32-bit No
ZIP/JAR entry times (DOS format) 1980-based, upper limit 2107 (JDK-8161942) [11] Until 2107 (separate limit)

Databases matter too. MySQL's TIMESTAMP type and, before 8.0.28, UNIX_TIMESTAMP() stop at 2038-01-19 03:14:07 UTC whatever JDBC driver you use [12]. See Databases.

Affected versions / products

Component Representation 2038 exposure
System.currentTimeMillis(), java.util.Date long ms None
java.time.Instant (Java 8+) long s + int ns None (±1 billion years)
Application int seconds 32-bit High (silent wrap)
32-bit HotSpot on glibc armhf/i386 native time_t Medium (JDK-8279444 open)
Keycloak Time.currentTime() int Deprecated in favour of currentTimeSeconds() (long) [3]

Risk & compliance notes

Detection & mitigation

  1. Static search: grep -RnE '\(int\)\s*\(?\s*(System\.currentTimeMillis|.*getEpochSecond|.*/\s*1000)'. Also look at .toInt() in Kotlin and Scala.
  2. Error Prone, SpotBugs or IntelliJ "narrowing conversion" inspections flag long→int narrowing in arithmetic contexts. Review every match that involves time.
  3. Use Math.toIntExact at unavoidable 32-bit boundaries so that overflow fails fast.
  4. Test with an injected java.time.Clock set to 2038-01-19T03:14:08Z and later. That's much easier than changing the system clock. See Testing-and-Detection.
  5. Retire 32-bit JVMs, or verify that their native layer was built with 64-bit time_t. See Remediation-Guide.

See also

References

  1. System.currentTimeMillis() — Java SE 21 API, Oracle — returns long milliseconds since 1970-01-01 UTC.
  2. java.time.Instant — Java SE 21 API, Oracle — MIN/MAX range, long seconds + int nanos storage, toEpochMilli throws ArithmeticException.
  3. org.keycloak.common.util.Time — Keycloak Javadoc — currentTime() (int) deprecated "to avoid integer overflow beyond year 2038".
  4. JDK-8279444: File.setLastModifiedTime(long) suffers from Year 2038 problem on 32-bit platforms — OpenJDK bug tracker, 2022-01-04 — open; affects 8–19.
  5. RFR: 8336529: (fs) UnixFileAttributeViews setTimes() failing on armhf, Ubuntu noble — OpenJDK nio-dev, 2024-07 — 32-bit time_t symbols left in glibc after the t64 transition.
  6. JNI Types and Data Structures — Java SE 21, Oracle — jint signed 32 bits, jlong signed 64 bits.
  7. Apache Avro 1.11.1 Specification — Apache Software Foundation — timestamp-millis/micros annotate long; date annotates int days.
  8. Protocol Buffers well-known types: Timestamp — protobuf.dev — int64 seconds, int32 nanos, range 0001–9999.
  9. MessagePack specification — msgpack/msgpack on GitHub — timestamp 32 range ends 2106-02-07 06:28:16 UTC; 64/96-bit forms.
  10. kotlin.time.Instant — Kotlin standard library API — epochSeconds: Long; since Kotlin 2.3.
  11. JDK-8161942: java.util.zip.ZipEntry not covering UpperLimit range of DOS epoch — OpenJDK bug tracker — DOS time upper limit around 2107.
  12. Date and Time Functions — MySQL 8.0 Reference Manual — UNIX_TIMESTAMP()/FROM_UNIXTIME() limits; 8.0.28 change on 64-bit.