# jpf-core turns the JVM into a model checker for Java bytecode

> Java PathFinder is an extensible software model checking framework that executes Java bytecode while exploring execution paths rather than one, so it can find concurrency defects such as deadlocks and unhandled exceptions such as NullPointerException and AssertionError. jpf-core is the base component every JPF project needs, it is built with Gradle, and the project ships a Docker path for getting it running.

**javapathfinder/jpf-core** — JPF is an extensible software analysis framework for Java bytecode. jpf-core is the basis for all JPF projects; you always need to install it. It contains the basic VM and model checking infrastructure, and can be used to check for concurrency defects like deadlocks, and unhandled exceptions like NullPointerExceptions and AssertionErrors.

- Repository: https://github.com/javapathfinder/jpf-core
- Stars: 620 · Forks: 417
- Language: Java
- License: not declared
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/javapathfinder-jpf-core

## What model checking means here

Java PathFinder is an extensible software model checking framework for Java bytecode programs, and the mechanism is the reason it finds things other tools miss.

A conventional test runs one path through the code. JPF executes the same bytecode while exploring the possible execution paths, and the description in the repository is that it can be used to check for concurrency defects like deadlocks, and unhandled exceptions like NullPointerExceptions and AssertionErrors.

That combination is the interesting part. A NullPointerException is not a hard finding to reproduce by hand, because it depends on a branch somewhere, often an interleaving between two threads, and manual testing finds the one case you happened to try. A systematic explorer over the same code will find the path that leads there.

The relationship to ordinary testing is that JPF is not a replacement for it. It is a complement, aimed at the class of bug where the defect is in the control flow rather than in a value.

The framework is extensible, and the repository is called jpf-core for a reason: it is the basis for all JPF projects, and the description is explicit that you always need to install it. The other JPF repositories are built on top of this one, so the core is the foundation you install first and the thing every extension assumes is present.

## The project layout, and what the core provides

The repository layout shows the shape of the build and where the configuration lives.

There is src/ for the sources, bin/ for the launch scripts, docs/ for documentation, and a gradle/ directory with the gradlew wrapper and gradlew.bat alongside build.gradle and settings.gradle. Two files at the root are worth noticing: jpf.properties, which is where JPF's own settings go, and output.txt.

The licence file is named LICENSE-2.0.txt, which is a bit unusual as a filename but is the Apache 2.0 text.

The Docker route is the practical entry point, and the README gives three steps. The image is built with:

```bash
docker-compose build
```

and started with:

```bash
docker-compose run --rm jpf-dev
```

and inside the container JPF itself is built with:

```bash
./gradlew clean build
```

The compose file explains the arrangement. The service is named jpf-dev, it builds from the local context, the working directory is /home/jpf-core, the repository is bind-mounted there so your edits are visible inside the container, and a second mount maps ~/.gradle to /root/.gradle so the Gradle cache survives a container rebuild rather than being downloaded every time.

The Dockerfile is built on eclipse-temurin:11-jdk-jammy, which pins both the JDK and the Linux distribution. It installs git, unzip and bash, creates the /home/jpf-core directory, and sets the working directory to it. There is one small but telling line: git config --global --add safe.directory /home/jpf-core. That is needed because the bind mount makes the directory owned by a different user than the one inside the container, and Git refuses to operate on such a repository without being told it is safe.

## The Java version transition, and the branch that still exists

There is a note in the README about the Java version, and it is the kind of thing that saves a day of confusion.

We have transitioned to OpenJDK with the new version of JPF, for Java 11, so unlike branch java-8, the new default version for Java 11 no longer compiles using Oracle's JDK.

So the project has two eras. The java-8 branch is the older line that builds against Oracle's JDK, and the default branch is the Java 11 line built against OpenJDK. The default branch is what the Dockerfile matches, with temurin 11 as the base image.

The transition is the reason for a related subtlety. A tool that executes bytecode needs to run the code it is checking, and the class file version, the API surface available to the program and the JVM doing the checking all have to line up. A project targeting Java 8 bytecode is not obviously analysable by a Java 11 VM, and that mismatch is what the two-branch structure exists to handle.

The repository is otherwise quiet about it. The README says all the latest developments, changes and documentation can be found on the project wiki, which is where the details of that arrangement live.

## Where to look when the build fails

The README spends a surprising amount of its space on troubleshooting, and the reason is a project with a long history and a specific setup.

If you are having problems installing and running JPF, the README points at the How to install JPF guide on the wiki. It then says the project has documented on the wiki a lot of common problems during the install and build processes reported by users, and asks that if you are facing an issue, you make sure they have not addressed it in documentation before opening one.

That is a real signal about the shape of the tool. JPF is a framework that others extend, so the build involves a Gradle project, a specific JDK, a properties file and a JVM that loads the classes under test. Each of those is a place where a mismatch produces a confusing error, and the wiki is where the accumulated answers live.

The support routes are listed too. You can contact the project at the java-pathfinder@googlegroups.com address, or open an issue on the issue tracker.

The documentation effort is acknowledged as ongoing. There is a constant effort to update and add JPF documentation on the wiki, and contributors who want to help with it are asked to contact the same group address. The README closes with the project's sign-off, Happy Verification, from the Java PathFinder team.

## Where jpf-core fits, and where it stops

The comparison worth making is with other ways of finding these defects, because JPF is not the cheapest way to find a NullPointerException and it is not a general purpose fuzzer.

Against a fuzzer, the difference is method. A fuzzer generates random inputs and looks for a crash, so it explores the input space and hopes to reach an interesting state. JPF explores the state space of the program, following the branches it can take given the inputs it has, so it systematically covers the interleavings and the branch combinations. For a concurrency defect, that is the difference between searching for a needle and reading the whole haystack. The cost is that JPF has to be able to execute the program, and it is usually much slower than a test run, so it is a tool for a specific question rather than something you run on every commit.

Against a static analyser, the difference is that static analysis reasons about all possible executions at once and gives you a diagnosis, while JPF gives you a concrete failing execution. A static analyser is cheaper and runs earlier; JPF tells you the exact sequence that breaks.

The practical limits are structural. JPF checks Java bytecode, so it cannot analyse code that does not run on a JVM, and the bytecode it checks has to be compatible with the VM doing the checking, which is what the java-8 and Java 11 branch split exists to manage. The state space explodes with concurrency, so a program with unbounded loops or large data sets needs careful configuration to stay tractable, and that configuration work is the real learning curve of using the tool.

And jpf-core is only the base. The description says it contains the basic VM and model checking infrastructure, and that it is the basis for all JPF projects, so a project that needs a particular analysis will also need the matching extension, and those are separate repositories with their own versions.

On maintenance the signals are mixed but not bad. The last push was on 2026-09-17 and the repository is not archived, but there are no GitHub releases, so there is no tagged version to pin, and the licence is recorded as unknown in the repository metadata even though the LICENSE-2.0.txt file is present. The project is also clearly a long-running one, with a Java 8 branch still maintained alongside the current line, which is a sign of a tool with users rather than a fresh experiment.

## Conclusion

jpf-core suits a Java team that needs to hunt for concurrency defects and unhandled exceptions in bytecode it can run, since the framework explores execution paths rather than one and the base component carries the VM and the model checking infrastructure. It is the wrong tool if your code does not run on the JVM you can instrument, because JPF executes the bytecode itself, and it is the wrong tool if you need a property-based fuzzer rather than a systematic path explorer. Before adopting it, read the How to install JPF guide on the project wiki, build with ./gradlew clean build, and check which JPF extensions your project needs, because jpf-core is the basis for all of them and you always need to install it.

## FAQ

### What is Java PathFinder used for?

It is an extensible software model checking framework for Java bytecode programs. jpf-core is the basis for all JPF projects and contains the basic VM and model checking infrastructure, and it can be used to check for concurrency defects like deadlocks and unhandled exceptions like NullPointerExceptions and AssertionErrors.

### How do I build jpf-core with Docker?

Run docker-compose build to create the image, then docker-compose run --rm jpf-dev to start a container, and inside it run ./gradlew clean build. The compose file bind-mounts the repository at /home/jpf-core and maps ~/.gradle to /root/.gradle so the build cache survives container rebuilds.

### Which Java version does jpf-core build against?

The default branch is the Java 11 version, built against OpenJDK rather than Oracle's JDK, and the Dockerfile uses eclipse-temurin:11-jdk-jammy. The older java-8 branch is the one that still compiles using Oracle's JDK.

### Where is the documentation for Java PathFinder?

On the project wiki at github.com/javapathfinder/jpf-core/wiki, which the README says holds all the latest developments, changes and documentation, including a How to install JPF guide and the common install and build problems reported by users.

### How do I report a problem with jpf-core?

The README asks that you check the wiki documentation first, since common install and build problems are already documented there. Otherwise you can contact java-pathfinder@googlegroups.com or open an issue on the issue tracker.

## Sources

- [Issues](https://github.com/javapathfinder/jpf-core/issues)
- [javapathfinder/jpf-core on GitHub](https://github.com/javapathfinder/jpf-core)
- [README](https://github.com/javapathfinder/jpf-core/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/javapathfinder-jpf-core
