Last week, I demonstrated that OpenJDK is faster than GraalVM Java, at least for obtaining the Java version. This even prompted the mighty Thomas Wuerthinger (creator of GraalVM) to react. But the measured ~20ms for the OpenJDK is still too slow for applications like execjar, where it could significantly increase the runtime of short-running CLI tools. In this week’s brief blog post, I’ll show you the fastest way to access the Java version.
The main performance issue is that calling java -version creates a process with a fairly large (around 38MB) maximum resident set size, and using a proper command line parser. But do we actually need to call the java binary to get the version?
TL;DR: I created the java-version tool, which can obtain the Java version in under a millisecond.
Basic Idea
No, we can just realize that most Java installations have a release file that contains the relevant information in a machine-readable format. You can find this file in the main folder of the installation (./release when java is in ./bin).
For my SapMachine 25 installation, the file looks like:
IMPLEMENTOR="SAP SE" IMPLEMENTOR_VERSION="SapMachine" JAVA_RUNTIME_VERSION="25+36-LTS" JAVA_VERSION="25" JAVA_VERSION_DATE="2025-09-16" LIBC="default" ...
The file is generated in the OpenJDK at build time via (source):
define create-info-file
$(if $(JDK_ARCH_ABI_PROP_NAME), \
$(call info-file-item, "SUN_ARCH_ABI", "$(JDK_ARCH_ABI_PROP_NAME)"))
$(call info-file-item, "SOURCE", "$(strip $(SOURCE_REVISION))")
$(call info-file-item, "IMPLEMENTOR", "$(COMPANY_NAME)")
$(if $(VENDOR_VERSION_STRING), \
$(call info-file-item, "IMPLEMENTOR_VERSION", "$(VENDOR_VERSION_STRING)"))
$(call info-file-item, "JAVA_VERSION_DATE", "$(VERSION_DATE)")
$(call info-file-item, "JAVA_RUNTIME_VERSION", "$(VERSION_STRING)")
$(call info-file-item, "OS_NAME", "$(RELEASE_FILE_OS_NAME)")
$(call info-file-item, "OS_ARCH", "$(RELEASE_FILE_OS_ARCH)")
$(call info-file-item, "LIBC", "$(RELEASE_FILE_LIBC)")
endef
For GraalVM, the file looks similar.
Let’s implement a java-version tool with this knowledge.
Implementation
I chose to implement the tool in C++ to avoid the overhead of any runtime. The implementation is fairly straightforward, especially with the modern filesystem API (C++ 17):
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <path-to-bin-java>\n";
return 1;
}
// Resolve the path, following any symbol links
fs::path javaPath;
try {
javaPath = fs::canonical(argv[1]);
} catch (const fs::filesystem_error& e) {
std::cerr << "Failed to resolve path: " << e.what() << "\n";
return 1;
}
// Expect .../bin/java
if (javaPath.filename() != "java" ||
javaPath.parent_path().filename() != "bin") {
std::cerr << "Path does not end with /bin/java\n";
return 1;
}
// ../release
fs::path releasePath = javaPath.parent_path().parent_path() / "release";
// Try to open the release file
std::ifstream file(releasePath);
if (!file) {
std::cerr << "Failed to open " << releasePath << "\n";
return 1;
}
const std::string key = "JAVA_VERSION=\"";
std::string line;
// Look for the JAVA_VERSION line
while (std::getline(file, line)) {
if (line.rfind(key, 0) == 0) { // starts with key
auto start = key.size();
auto end = line.find('"', start);
if (end != std::string::npos) {
std::cout << line.substr(start, end - start) << "\n";
return 0;
}
}
}
std::cerr << "JAVA_VERSION not found\n";
return 1;
}
You can find the whole file on GitHub and build it via:
g++ -std=c++17 java_version.cpp -o java_version
Usage
The usage is pretty simple:
./java-version `which java` # or directly ./java-version java/25-sapmchn/bin/java
Benchmarks
The most important part of this blog post is, of course, the comparison with java -version (although you already saw it in the blog post OpenJDK is faster than GraalVM Java*). For the benchmark, I used hyperfine on my MacBook Pro M5:

Benchmark 1: java -version Time (mean ± σ): 19.8 ms ± 1.5 ms [User: 11.1 ms, System: 10.7 ms] Range (min … max): 17.1 ms … 31.1 ms 1000 runs Benchmark 2: java-version java/25-sapmchn/bin/java Time (mean ± σ): 925.1 µs ± 418.2 µs [User: 466.3 µs, System: 373.8 µs] Range (min … max): 425.3 µs … 5137.1 µs 1000 runs Summary java-version java/25-sapmchn/bin/java ran 21.41 ± 9.81 times faster than java -version
It’s probably as fast as you can get.
Conclusion
Sometimes the fastest solution is to find the information you need in a different place. I will admit that the tool solves a problem that most people don’t have, but solving it helped improve the startup time of my tools.
I hope you still found the second part of my mini-series on java -version interesting, and I can assure you it was the last. The next blog post will be on reading and writing JFR files programmatically.
This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone.
P.S.: Stay warm in the cold and let the flow guide you.





