Transpilation
The core pipeline: pick methods, lower them to native code that behaves exactly like the JVM would, and repackage the JAR so nothing changes for the end user.
Pick what becomes native by annotation simple-name (any annotation
works, no API jar required), in include or
exclude mode, narrowed further by path-glob filters.
Output is a fat JAR plus one JAR per target triple, each carrying a
standard JNI library (.so / .dll /
.dylib). The injected NativeLoader picks the
matching binary at runtime.
Lowering follows HotSpot's own zero-interpreter and JIT internals. Integer overflow, NaN propagation, exception ordering, and stack effects match the JVM specification.
<init> and <clinit> bodies are
split into synthetic methods that can be transpiled too. Synthetic
names follow a configurable scheme (hash,
alphabetic, random).
Calls into Math, StrictMath,
Integer, Long, Float,
Double, Unsafe, and others become direct
native equivalents instead of a JNI round-trip.
Map any owner | name | desc to a C++ function in one of
your headers, with options for JNIEnv*, exception
checking, return casts, and skipping the receiver argument.
Virtualization (VM backend)
Virtualization replaces a method's bytecode with an encrypted program for a bundled interpreter, so the original logic never appears as native instructions a disassembler can read. It is the strongest protection jvmtp applies and the most expensive at run time, so it is off by default and selected per method. A method the backend cannot virtualize yet falls back to a normal native method, so enabling it never fails a build.
The default virtualizing machine. Selected methods run as encrypted
programs on a bundled stack-based interpreter with full ISA coverage,
including invokedynamic.
A second machine alongside the stack interpreter. A method lowers to a register form covering the same constructs. One binary can mix both, by filter or a per-build ratio split.
Emit a method on both machines in one wrapper and choose which runs per call at run time, so no single execution trace observes both forms and the two share no opcode space.
vm.harden-interpreter obfuscates the dispatch loop itself;
vm.lazy-decrypt decrypts one chunk at a time as control
flow reaches it, so untaken branches stay ciphertext in memory.
Opcode permutation, operand encoding, handler mutation (with genuine MBA), instruction fragmentation, fusion, and junk insertion make one build's VM differ from the next, so analysis does not transfer.
Multiple independent dispatch tables, reshuffled periodically at run time, so reversing one instance's mapping does not reveal the others and no single trace observes them all.
Native obfuscation
Independent, opt-in modules that make the generated native code harder to read. Enable the master switch, then turn on the modules you want. They apply to transpiled methods and to the native intrinsics support code, and can be scoped globally or per module with include/exclude lists.
Integer and long literals no longer appear directly in the binary.
obfuscation.constants.mba conceals them through
mixed-boolean-arithmetic an optimizer cannot fold back.
Java string literals, and the identifier strings in the intrinsics support code, are kept out of the binary as readable text and reconstructed only on first use.
Call targets resolve indirectly at runtime instead of as plain calls, with variants to inline the cipher per site, cover plain native calls, or route through a shared per-signature dispatcher.
Opaque predicates hide conditional branches, and flattening restructures control flow into a form much harder to follow than the original structured code.
Integer arithmetic and bitwise operations are rewritten into more complex equivalent forms that resist analysis.
obfuscation.jumps turns comparisons into branchless
arithmetic and transfers control through a register, and can emulate a
call with a jump where the toolchain allows it.
Runtime protection
Checks and hardening that run inside the shipped binary, woven into load-time init and re-run from the VM interpreter's own hot path. Each family is conditionally compiled, so a build with it off carries none of the code.
OS-level checks (attached debugger, disabled ASLR,
LD_PRELOAD, instrumentation modules, hooked libc, int3
self-scan) and Java agent detection (JVMTI / JDWP / instrumentation).
A positive check marks the process rather than exiting on the spot; the process terminates a moment later at a shared point, so no single check site maps 1:1 to the exit.
Restores the original JNIEnv* function table on method
entry to defeat agent-style JNI pointer hooks. The aggressive mode
re-checks periodically, catching a hook installed after load.
Anti-debug and environment checks re-run from the interpreter hot path, so a debugger or agent that attaches after startup is caught, the same as one already present when the process loaded.
Every field and method ID a class owns resolves together on first use, off the per-instruction path, so no single lookup is tied to a specific instruction a debugger is watching.
Packs the per-platform libraries into one compressed blob, streamed back out at startup by the injected loader, for a smaller and less discoverable output JAR.
Build & toolchain
One host builds every platform. jvmtp drives the C++ build itself and hooks into external protectors where you want stronger native protection than it provides on its own.
Zig is downloaded automatically on first run and brings its own linker
and headers. One config.conf produces libraries for every
configured triple from any host, no host-side SDK required.
Per-file compiles run in parallel with a configurable job cap, a per-invocation timeout that kills a hung compiler, and automatic retries that ride out a transient stall without retrying a real error.
compiler.vmp-support wraps each transpiled function in a
marker labeled with its qualified name, links the VMProtect SDK, and
jvmtp repack merges the protected binaries back in.
jvmtp sits in front of bytecode obfuscators (ZKM and the like) and native protectors such as Themida. It moves methods into native code rather than replacing a protector.
Supported platforms
| triple | os / environment | status |
|---|---|---|
| x86_64-linux | Linux (glibc) | stable |
| x86_64-windows | Windows 10+ | stable |
| aarch64-macos | macOS (Apple Silicon) | stable |
| aarch64-windows | Windows on ARM | stable |
| aarch64-linux | Linux | experimental |
| x86_64-macos | macOS (Intel) | experimental |
Triples are configured via target.platforms in
config.conf. Any JVM implementing the JNI specification runs
the output (HotSpot, OpenJ9, GraalVM, Android ART).
Next
Every key behind these features is documented in the configuration reference. To try jvmtp on your own JAR, start with getting started, or request an evaluation sample with sample transpiled output. Licensing terms are on the license page.