This plugin for the Cloud Foundry Command Line provides convenience utilities to work with Java applications deployed on Cloud Foundry.
Currently, it allows you to:
- Trigger and retrieve a heap dump and a thread dump from an instance of a Cloud Foundry Java application
- To run jcmd remotely on your application
- To start, stop and retrieve JFR and async-profiler (SapMachine only) profiles from your application
- To run jstall for one-shot JVM inspection (deadlock detection, hot threads, dependency graphs, and more) — bundled directly in the plugin, requires Java 17+ locally
Make sure you have the CF Community plugin repository configured (or add it via
cf add-plugin-repo CF-Community http://plugins.cloudfoundry.org)
Trigger installation of the plugin via
cf install-plugin javaThe releases in the community repository are older than the actual releases on GitHub, that you can install manually, so we recommend the manual installation.
Download the latest release from GitHub.
To install a new version of the plugin, run the following:
# on Mac arm64
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/latest/download/cf-cli-java-plugin-macos-arm64
# on Windows x86
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/latest/download/cf-cli-java-plugin-windows-amd64
# on Linux x86
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/latest/download/cf-cli-java-plugin-linux-amd64You can verify that the plugin is successfully installed by looking for java in the output of cf plugins.
Download the current snapshot release from GitHub. This is intended for experimentation and might fail.
To install a new version of the plugin, run the following:
# on Mac arm64
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/download/snapshot/cf-cli-java-plugin-macos-arm64
# on Windows x86
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/download/snapshot/cf-cli-java-plugin-windows-amd64
# on Linux x86
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/download/snapshot/cf-cli-java-plugin-linux-amd64This plugin internally uses jmap for OpenJDK-like Java virtual machines. When using the
Cloud Foundry Java Buildpack, jmap is no longer shipped by default
in order to meet the legal obligations of the Cloud Foundry Foundation. To ensure that jmap is available in the
container of your application, you have to explicitly request a full JDK in your application manifest via the
JBP_CONFIG_OPEN_JDK_JRE environment variable. This could be done like this:
---
applications:
- name: <APP_NAME>
memory: 1G
path: <PATH_TO_BUILD_ARTIFACT>
buildpack: https://github.com/cloudfoundry/java-buildpack
env:
JBP_CONFIG_OPEN_JDK_JRE:
'{ jre: { repository_root: "https://java-buildpack.cloudfoundry.org/openjdk-jdk/jammy/x86_64", version: 11.+ }
}'
JBP_CONFIG_JAVA_OPTS: "[java_opts: '-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints']"-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints is used to improve profiling accuracy and has no known negative
performance impacts.
Please note that this requires the use of an online buildpack (configured in the buildpack property). When system
buildpacks are used, staging will fail with cache issues, because the system buildpacks don’t have the JDK cached.
Please also note that this is not to be considered a recommendation to use a full JDK. It's just one option to get the
tools required for the use of this plugin when you need it, e.g., for troubleshooting. The version property is
optional and can be used to request a specific Java version.
As it is built directly on cf ssh, the cf java plugin can work only with Cloud Foundry applications that have
cf ssh enabled. To check if your app fulfills the requirements, you can find out by running the
cf ssh-enabled [app-name] command. If not enabled yet, run cf enable-ssh [app-name].
Note: You must restart your app after enabling SSH access.
In case a proxy server is used, ensure that cf ssh is configured accordingly. Refer to the
official documentation of the Cloud Foundry
Command Line for more information. If cf java is having issues connecting to your app, chances are the problem is in
the networking issues encountered by cf ssh. To verify, run your cf java command in "dry-run" mode by adding the
-n flag and try to execute the command line that cf java gives you back. The plugin now wraps common SSH failures
with user-facing guidance instead of printing the raw ssh error verbatim, so running the generated cf ssh command
directly is the quickest way to inspect the underlying transport error. If that direct command fails, the issue is not
in cf java, but in whatever makes cf ssh fail.
Getting a heap-dump:
> cf java heap-dump $APP_NAME
-> ./$APP_NAME-heapdump-$RANDOM.hprofGetting a thread-dump:
> cf java thread-dump $APP_NAME
...
Full thread dump OpenJDK 64-Bit Server VM ...
...Creating a CPU-time profile via async-profiler:
> cf java asprof-start-cpu $APP_NAME
Profiling started
# wait some time to gather data
> cf java asprof-stop $APP_NAME
-> ./$APP_NAME-asprof-$RANDOM.jfrRunning arbitrary JCMD commands, like VM.uptime:
> cf java jcmd $APP_NAME --args 'VM.uptime'
Connected to remote JVM
JVM response code = 0
$TIME sRunning JStall for quick JVM inspection (requires Java 17+ locally):
# Default: run status analysis with deadlock detection, hot threads, etc.
> cf java jstall $APP_NAME
# Run a specific jstall subcommand (must include target, typically 'all')
> cf java jstall $APP_NAME --args 'deadlock all'
> cf java jstall $APP_NAME --args 'most-work --dumps 3 all'
> cf java jstall $APP_NAME --args 'flame all'Recording JVM diagnostic data for later analysis or sharing:
# Record all JVM diagnostic data into a zip file (default: APP_NAME-status.zip)
> cf java record-status $APP_NAME
# Record to a specific output file
> cf java record-status $APP_NAME diagnostics.zip
# Record with full data (including expensive jcmd commands, flame graph, and JFR)
> cf java record-status $APP_NAME --full
# Replay the recording locally with jstall
> jstall -f diagnostics.zip status all
> jstall -f diagnostics.zip threads allWhen using jcmd and asprof commands with the --args parameter, the following variables are automatically replaced
in your command strings:
@FSPATH: A writable directory path on the remote container (always set, typically/tmp/jcmdor/tmp/asprof)@ARGS: The command arguments you provided via--args@APP_NAME: The name of your Cloud Foundry application@FILE_NAME: Generated filename for file operations (includes full path with UUID)
Example usage:
# Create a heap dump in the available directory
cf java jcmd $APP_NAME --args 'GC.heap_dump @FSPATH/my_heap.hprof'
# Use an absolute path instead
cf java jcmd $APP_NAME --args "GC.heap_dump /tmp/absolute_heap.hprof"
# Access the application name in your command
cf java jcmd $APP_NAME --args 'echo "Processing app: @APP_NAME"'Note: Variables use the @ prefix to avoid shell expansion issues. The plugin automatically creates the @FSPATH
directory and downloads any files created there to your local directory (unless --no-download is used).
The following is a list of all available commands (some of the SapMachine specific), generated via cf java --help:
NAME:
java - Obtain a heap-dump, thread-dump or profile from a running, SSH-enabled Java application.
USAGE:
cf java COMMAND APP_NAME [options]
heap-dump
Generate a heap dump from a running Java application
thread-dump
Generate a thread dump from a running Java application
vm-info
Print information about the Java Virtual Machine running a Java
application
jcmd (supports --args)
Run a JCMD command on a running Java application via --args, downloads
and deletes all files that are created in the current folder, use
'--no-download' to prevent this. Environment variables available:
@FSPATH (writable directory path, always set), @ARGS (command
arguments), @APP_NAME (application name), @FILE_NAME (generated filename
with UUID for file operations), and @STATIC_FILE_NAME (without UUID).
Use single quotes around --args to prevent shell expansion.
jfr-start
Start a Java Flight Recorder default recording on a running Java
application (stores in the container-dir)
jfr-start-profile
Start a Java Flight Recorder profile recording on a running Java
application (stores in the container-dir)
jfr-start-gc (recent SapMachine only)
Start a Java Flight Recorder GC recording on a running Java application
(stores in the container-dir)
jfr-start-gc-details (recent SapMachine only)
Start a Java Flight Recorder detailed GC recording on a running Java
application (stores in the container-dir)
jfr-stop
Stop a Java Flight Recorder recording on a running Java application
jfr-dump
Dump a Java Flight Recorder recording on a running Java application
without stopping it
jfr-status
Check the running Java Flight Recorder recording on a running Java
application
vm-version
Print the version of the Java Virtual Machine running a Java application
vm-vitals
Print vital statistics about the Java Virtual Machine running a Java
application
asprof (recent SapMachine only, supports --args)
Run async-profiler commands passed to asprof via --args, copies files in
the current folder. Don't use in combination with asprof-* commands.
Downloads and deletes all files that are created in the current folder,
if not using 'start' asprof command, use '--no-download' to prevent
this. Environment variables available: @FSPATH (writable directory path,
always set), @ARGS (command arguments), @APP_NAME (application name),
@FILE_NAME (generated filename for file operations), and
@STATIC_FILE_NAME (without UUID). Use single quotes around --args to
prevent shell expansion.
asprof-start-cpu (recent SapMachine only)
Start an async-profiler CPU-time profile recording on a running Java
application
asprof-start-wall (recent SapMachine only)
Start an async-profiler wall-clock profile recording on a running Java
application
asprof-start-alloc (recent SapMachine only)
Start an async-profiler allocation profile recording on a running Java
application
asprof-start-lock (recent SapMachine only)
Start an async-profiler lock profile recording on a running Java
application
asprof-stop (recent SapMachine only)
Stop an async-profiler profile recording on a running Java application
asprof-status (recent SapMachine only)
Get the status of async-profiler on a running Java application
status (requires Java 17+ locally, supports --args, --full)
Quick status check of the remote JVM: deadlock detection, hot threads,
dependency graph, and more. Requires Java 17+ locally. Use --full for
comprehensive analysis. Pass additional options via --args (e.g.,
'--dumps 3'). See https://github.com/parttimenerd/jstall
jstall (requires Java 17+ locally, supports --args)
Inspect the remote JVM via JStall (runs on your machine, connects via cf
ssh). Requires Java 17+ locally. Pass jstall subcommands and options via
--args (default: 'status all'). See
https://github.com/parttimenerd/jstall
record-status (requires Java 17+ locally, supports --args, --full)
Record diagnostic data from the remote JVM via JStall and save to a
local zip file. Requires Java 17+ locally. Output file can be specified
as a trailing argument (default: APP_NAME-status.zip). Use --full for
comprehensive recording. See https://github.com/parttimenerd/jstall
OPTIONS:
--app-instance-index -i [index], select to which instance of the app to connect
--args -a, Miscellaneous arguments to pass to the command (if supported) in the
container, be aware to end it with a space if it is a simple option. For
commands that create arbitrary files (jcmd, asprof), the environment
variables @FSPATH, @ARGS, @APP_NAME, @FILE_NAME, and @STATIC_FILE_NAME are
available in --args to reference the working directory path, arguments,
application name, and generated file name respectively.
--container-dir -cd, the directory path in the container that the heap dump/JFR/... file will be
saved to
--dry-run -n, just output to command line what would be executed
--full -f, enable full mode for more comprehensive JVM analysis (only for status and
record-status)
--keep -k, keep the heap dump in the container; by default the heap dump/JFR/... will
be deleted from the container's filesystem after being downloaded
--local-dir -ld, the local directory path that the dump/JFR/... file will be saved to,
defaults to the current directory
--no-download -nd, don't download the heap dump/JFR/... file to local, only keep it in the
container, implies '--keep'
--verbose enable verbose output for the plugin (note: -v is reserved by CF CLI)
The --args parameter passes values directly into remote shell commands via cf ssh. This is by design to support
shell features like environment variable expansion and piping. Do not pass untrusted input to --args — treat
it with the same caution as a shell command.
The heap dumps and profiles will be downloaded to a local file automatically (to the current directory by default).
Use --local-dir to specify a different download location. To save disk space of
the application container, the files are automatically deleted unless the -keep option is set.
Providing -container-dir is optional. If specified the plugin will create the heap dump or profile at the given file
path in the application container. Without providing this parameter, the file will be created either at /tmp or at the
file path of a file system service if attached to the container.
cf java [heap-dump|stop-jfr|stop-asprof] [my-app] -local-dir /local/path [-container-dir /var/fspath]Everything else, like thread dumps, will be output to std-out. You may want to redirect the command's output to file,
e.g., by executing:
cf java thread-dump [my_app] -i [my_instance_index] > thread-dump.txtThe -k flag is invalid when invoking non file producing commands. (Unlike with heap dumps, the JVM does not need to
output the thread dump to file before streaming it out.)
Some commands depend on writable filesystem space inside the application container. In particular,
cf java heap-dump, cf java asprof-stop, and cf java jfr-stop first create a file in the container, then stream
that file back over SSH, and finally remove it again unless the -k flag is set.
The available container filesystem space is controlled by the Cloud Foundry landscape configuration and may be limited. Heap dumps can be large, roughly scaling with heap usage, and profile files can also grow substantially depending on recording duration and settings. If the container does not have enough free space, the dump or recording cannot be created and the command will fail.
The plugin is also constrained by limitations in the current cf-cli plugin framework:
- There is no distinction between
stdoutandstderroutput from the underlyingcf sshcommand (see this issue on thecf-cliproject)cf javawill still usually exit with status code1when the underlyingcf sshcommand fails- If you need separate
stdoutandstderr, run the plugin in dry-run mode (--dry-run) and execute the generated command directly
The jstall flame command may fail with an error like:
Error: profiling was skipped: profiling-failed
jstall execution failed: exit status 1
Reason: Flame graph generation depends on low-level profiling capabilities such as perf events. These are often
restricted in containerized environments for security reasons.
Workarounds:
-
Use async-profiler directly for CPU profiling:
cf java asprof-start-cpu $APP_NAME # ... wait for profiling ... cf java asprof-stop $APP_NAME
-
Use other jstall commands that don't require perf:
cf java jstall $APP_NAME --args 'status all' # JVM status & diagnostics cf java jstall $APP_NAME --args 'deadlock all' # Deadlock detection cf java jstall $APP_NAME --args 'threads all' # Thread information
-
Record diagnostic data for later analysis:
cf java record-status $APP_NAME diagnostics.zip # Then replay locally jstall -f diagnostics.zip status all
When the plugin cannot establish SSH connectivity, it reports a categorized error message with likely causes and
suggested next steps instead of only printing the raw cf ssh transport error. A typical message looks like:
Cannot connect to app 'APP_NAME' via SSH.
Possible causes and solutions:
1. SSH may not be enabled on the application. Try:
cf enable-ssh APP_NAME
cf restart APP_NAME
2. Check your network connection and firewall settings.
3. Verify the application is running: cf app APP_NAME
Common causes and fixes:
| Error | Likely Cause | Solution |
|---|---|---|
connection refused or not enabled |
SSH not enabled on application | cf enable-ssh APP_NAME && cf restart APP_NAME |
connection reset |
Network interruption | Retry the command; check internet connection |
timeout |
Network unreachable | Check firewall/proxy settings; verify platform connectivity |
Permission denied |
Authentication failed | cf logout && cf login with correct credentials |
Debugging: If you need the original SSH transport error from Cloud Foundry, run cf ssh APP_NAME -c 'echo ok'
directly.
Storing dumps or profile recordings to the filesystem may lead to to not enough space on the filesystem been available for other tasks (e.g., temp files). In that case, the application in the container may suffer unexpected errors.
Executing a thread dump via the cf java command does not have much of an overhead on the affected JVM. (Unless you
have a lot of threads, that is.)
Heap dumps, on the other hand, have to be treated with a little more care. First of all, triggering the heap dump of a JVM makes the latter execute in most cases a full garbage collection, which will cause your JVM to become unresponsive for the duration. How much time is needed to execute the heap dump, depends on the size of the heap (the bigger, the slower), the algorithm used and, above all, whether your container is swapping memory to disk or not (swap is bad for the JVM). Since Cloud Foundry allows for over-commit in its cells, it is possible that a container would begin swapping when executing a full garbage collection. (To be fair, it could be swapping even before the garbage collection begins, but let's not knit-pick here.) So, it is theoretically possible that execuing a heap dump on a JVM in poor status of health will make it go even worse.
Profiles might cause overhead depending on the configuration, but the default configurations typically have a limited overhead.
# Setup environment and build
./setup-dev-env.sh
make build
# Run all quality checks and tests
./scripts/lint-all.sh ci
# Auto-fix formatting before commit
./scripts/lint-all.sh fixJStall Version: By default, the build downloads the latest stable JStall release. To test with the latest development build from GitHub Actions instead, use:
JSTALL_DEV=1 make build
# Or download a specific GitHub Actions run by ID
JSTALL_DEV=<run-id> make buildThis pulls the latest JStall build directly from the GitHub Actions artifacts instead of the released version.
Python Tests: Modern pytest-based test suite.
cd test && ./setup.sh && ./test.py allThe Python test runner in test/ supports resuming tests from any point using the --start-with option:
./test.py --start-with TestClass::test_method all # Start with a specific test (inclusive)This is useful for long test suites or after interruptions. See test/README.md for more details.
Centralized linting scripts:
./scripts/lint-all.sh check # Quality check
./scripts/lint-all.sh fix # Auto-fix formatting
./scripts/lint-all.sh ci # CI validation- Multi-platform builds (Linux, macOS, Windows)
- Automated linting and testing on PRs
- Pre-commit hooks with auto-formatting
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. Just be aware that this plugin is limited in scope to keep it maintainable. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.
See CHANGELOG.md for a detailed list of changes.
Copyright 2017 - 2025 SAP SE or an SAP affiliate company and contributors. Please see our LICENSE for copyright and license information.