This guide explains how to implement new JRE providers for the Cloud Foundry Java Buildpack.
- Overview
- Available JRE Providers
- JRE Interface
- BaseJRE — Shared Implementation
- Implementation Steps
- Examples
- Helper Functions
- Runtime Components
- Testing JREs
- Troubleshooting
A JRE provider:
- Detects when it should be used (env vars / config)
- Supplies the Java runtime (download + extract)
- Finalizes configuration (JAVA_HOME, JVM options)
Supply(), Finalize(), memory calculator, and JVMKill agent are all handled by BaseJRE. Implementing a new JRE is typically 14 lines.
| Provider | Key | Default | dirPrefixes / dirExacts |
|---|---|---|---|
| OpenJDK | openjdk |
Yes (fallback) | ["jdk", "jre"] |
| Zulu | zulu |
No | ["zulu"] |
| GraalVM | graalvm |
No | ["graalvm"] |
| IBM JRE | ibm |
No | prefixes: ["ibm-java"], exacts: ["jre"] |
| Oracle JRE | oracle |
No | ["jdk", "jre"] |
| SapMachine | sapmachine |
No | ["sapmachine"] |
| Azul Platform Prime | zing |
No | (custom — does not use BaseJRE) |
All providers implement jres.JRE (src/java/jres/jre.go):
type JRE interface {
Name() string
Detect() (bool, error)
Supply() error
Finalize() error
JavaHome() string
Version() string
// MemoryCalculatorCommand returns the shell snippet prepended to the
// container startup command; "" when the memory calculator is not installed.
MemoryCalculatorCommand() string
}Providers receive a *common.Context with shared dependencies (stager, manifest, installer, logger).
BaseJRE (src/java/jres/base_jre.go) provides the full Supply()/Finalize() implementation. Concrete JREs embed it and inject variation via constructor fields — never override Supply or Finalize.
| Field | Type | Purpose |
|---|---|---|
jreName |
string |
Display name in logs (e.g. "OpenJDK") |
jreKey |
string |
Manifest key and env detection (e.g. "openjdk") |
dirPrefixes |
[]string |
Directory name prefixes for findJavaHome() (e.g. ["jdk", "jre"]) |
dirExacts |
[]string |
Exact directory names for findJavaHome() (e.g. ["jre"] for IBM) |
installErrNote |
string |
Extra context appended to install error (e.g. GraalVM repo hint) |
extraFinalizeOpts |
func() string |
JRE-specific JVM opts written during Finalize() (e.g. -XX:ActiveProcessorCount=$(nproc)) |
- Download and extraction via
ctx.Installer.InstallDependency findJavaHome()— scansdirPrefixes/dirExactsthen falls back tojreDiritselfprofile.d/java.sh— exportsJAVA_HOMEat runtimeJAVA_HOMEenv file andbin/javadependency link- JVMKill agent (
Supply+Finalize) - Memory calculator (
Supply+Finalize) - Base JVM opts (
-Djava.io.tmpdir=$TMPDIR) extraFinalizeOpts(if set)
package jres
import "github.com/cloudfoundry/java-buildpack/src/java/common"
// MyJRE implements the JRE interface for My JRE.
type MyJRE struct{ BaseJRE }
// NewMyJRE creates a new My JRE provider.
func NewMyJRE(ctx *common.Context) *MyJRE {
b := newBaseJRE(ctx, "My JRE", "my-jre", []string{"myjre"}, nil, "")
b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" }
return &MyJRE{b}
}Set extraFinalizeOpts only if you need JRE-specific JVM flags. Omit it (or set to nil) if none are needed.
- name: my-jre
version: 1.2.3
uri: https://example.com/my-jre-1.2.3.tar.gz
sha256: <sha256>
cf_stacks:
- cflinuxfs4
- cflinuxfs5Also add to default_versions if it should be the default for a stack:
- name: my-jre
version: 1.xjreRegistry.Register(jres.NewMyJRE(ctx))That is all that is required.
Tarball extracts to a jdk-* or jre-* subdirectory. dirPrefixes covers both.
// src/java/jres/openjdk.go
type OpenJDKJRE struct{ BaseJRE }
func NewOpenJDKJRE(ctx *common.Context) *OpenJDKJRE {
b := newBaseJRE(ctx, "OpenJDK", "openjdk", []string{"jdk", "jre"}, nil, "")
b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" }
return &OpenJDKJRE{b}
}IBM JRE tarball may extract to a directory named jre exactly, or to ibm-java-*. Both patterns covered:
// src/java/jres/ibm.go
type IBMJRE struct{ BaseJRE }
func NewIBMJRE(ctx *common.Context) *IBMJRE {
b := newBaseJRE(ctx, "IBM JRE", "ibm", []string{"ibm-java"}, []string{"jre"}, "")
b.extraFinalizeOpts = func() string { return "-Xtune:virtualized -Xshareclasses:none" }
return &IBMJRE{b}
}GraalVM is not in the default manifest and requires repository_root config. The hint appears in the error if installation fails:
// src/java/jres/graalvm.go
type GraalVMJRE struct{ BaseJRE }
func NewGraalVMJRE(ctx *common.Context) *GraalVMJRE {
b := newBaseJRE(ctx, "GraalVM", "graalvm", []string{"graalvm"}, nil,
"(ensure repository_root is configured)")
b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" }
return &GraalVMJRE{b}
}If no JRE-specific opts are needed, omit extraFinalizeOpts:
type MinimalJRE struct{ BaseJRE }
func NewMinimalJRE(ctx *common.Context) *MinimalJRE {
b := newBaseJRE(ctx, "Minimal JRE", "minimal", []string{"jre"}, nil, "")
// extraFinalizeOpts left nil — BaseJRE writes only -Djava.io.tmpdir=$TMPDIR
return &MinimalJRE{b}
}Tests live in src/java/jres/. Use standard_jres_test.go as a reference — it tests all BaseJRE-based providers with shared helpers.
var _ = Describe("MyJRE", func() {
var (
ctx *common.Context
myJRE *jres.MyJRE
// ... temp dirs
)
BeforeEach(func() {
// set up ctx with temp dirs and mock manifest
myJRE = jres.NewMyJRE(ctx)
})
It("detects when JBP_CONFIG_MY_JRE is set", func() {
os.Setenv("JBP_CONFIG_MY_JRE", "{}")
defer os.Unsetenv("JBP_CONFIG_MY_JRE")
detected, err := myJRE.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(detected).To(BeTrue())
})
})BaseJRE.Detect() calls DetectJREByEnv(jreKey) which returns true when either:
- the documented
JBP_CONFIG_<NAME>_JREname is set (e.g.JBP_CONFIG_OPEN_JDK_JRE) — the recommended form, defined for each built-in provider in thejreNameToDocumentedEnvVarmap, or - the auto-generated
JBP_CONFIG_<KEY>alias is set —jreKeyuppercased with-→_(e.g.JBP_CONFIG_OPENJDK).
The documented _JRE names originate from the Ruby buildpack and are kept for backward compatibility, but they remain the recommended, fully-supported form: the buildpack points users to them, and (unlike the auto-generated alias) they also drive memory-calculator config. JBP_CONFIG_COMPONENTS is deprecated and is not used for JRE selection; there is no BP_JAVA_<JREKEY> detection variable.
A new custom JRE responds only to the auto-generated JBP_CONFIG_<KEY> name unless you add an entry to jreNameToDocumentedEnvVar.
# JRE unit tests only
.bin/ginkgo -r -mod vendor src/java/jres/
# Full unit suite
bash scripts/unit.shThese are available in src/java/jres/ and called by BaseJRE internally. You may need them if you implement a JRE that does not use BaseJRE (e.g. Zing).
| Function | Purpose |
|---|---|
GetJREVersion(ctx, jreKey) |
Resolves version from BP_JAVA_VERSION, then the documented JBP_CONFIG_<NAME>_JRE or auto-generated JBP_CONFIG_<KEY> alias, then manifest default |
DetectJREByEnv(jreKey) |
Returns true if the documented JBP_CONFIG_<NAME>_JRE name or the auto-generated JBP_CONFIG_<KEY> alias selects this JRE |
WriteJavaHomeProfileD(ctx, jreDir, javaHome) |
Writes profile.d/java.sh exporting JAVA_HOME, JRE_HOME, and PATH |
WriteJavaOpts(ctx, opts) |
Appends opts to the centralized .opts file consumed by profile.d/00_java_opts.sh |
common.DetermineJavaVersion(javaHome) |
Reads $JAVA_HOME/release → returns Java major version as int |
BP_JAVA_VERSION(e.g.21,21.*,21.0.5)- The documented
JBP_CONFIG_<NAME>_JREname, or the auto-generatedJBP_CONFIG_<KEY>alias, with aversion:field - Manifest
default_versionsentry for this JRE key
WriteJavaHomeProfileD produces $DEPS_DIR/<idx>/.profile.d/java.sh:
export JAVA_HOME=$DEPS_DIR/0/jre/jdk-21.0.5
export JRE_HOME=$DEPS_DIR/0/jre/jdk-21.0.5
export PATH=$JAVA_HOME/bin:$PATHBaseJRE installs and finalizes these automatically. Documented here for operational reference.
Downloads java-buildpack-memory-calculator binary. At application start it computes JVM heap/metaspace/stack settings from $MEMORY_LIMIT:
-Xmx512M -Xms512M -XX:MaxMetaspaceSize=128M -Xss1M -XX:ReservedCodeCacheSize=32M
User customization — prefer JBP_CONFIG_<JRE>_JRE for structured config (covers all three knobs):
cf set-env myapp JBP_CONFIG_OPEN_JDK_JRE \
'{memory_calculator: {stack_threads: 300, class_count: 500, headroom: 10}}'stack_threads — number of user threads (default: 250); affects -Xss heap budget.
class_count — estimated loaded classes (default: auto-detected); affects -XX:MaxMetaspaceSize.
headroom — percent of total memory to leave unallocated (default: 0).
MEMORY_CALCULATOR_* env vars are a simpler alternative, but only cover two of the three knobs. They take precedence over JBP_CONFIG_* when both are set:
cf set-env myapp MEMORY_CALCULATOR_STACK_THREADS 300
cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10
# class_count not available as a MEMORY_CALCULATOR_* env var — use JBP_CONFIG_* for thatNative agent (.so) that kills the JVM on OutOfMemoryError or memory allocation failure, causing CF to restart the container cleanly instead of hanging.
Added to JAVA_OPTS as:
-agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=printHeapHistogram=1
Heap dump support: if a volume service tagged heap-dump is bound, JVMKill writes heap dumps to the mounted volume:
cf bind-service myapp my-nfs -c '{"mount":"/volumes/heap-dumps","tags":["heap-dump"]}'findJavaHome fails — tarball extracted to directory matching neither dirPrefixes nor dirExacts. Check layout:
tar tf my-jre.tar.gz | head -5Add the top-level directory prefix/name to dirPrefixes/dirExacts accordingly.
Install fails with "No matching dependency" — jreKey in newBaseJRE must match the name: field in manifest.yml exactly.
extraFinalizeOpts not applied — set the function on b (the BaseJRE value) before wrapping: b.extraFinalizeOpts = func() string { ... }. Setting it after return &MyJRE{b} has no effect.
JAVA_HOME not set at runtime — verify profile.d/java.sh exists:
cf ssh myapp -- cat /home/vcap/deps/0/.profile.d/java.shMemory calculator not running — verify binary exists and MEMORY_LIMIT is set:
cf ssh myapp -- ls /home/vcap/deps/0/jre/bin/java-buildpack-memory-calculator-*
cf ssh myapp -- echo $MEMORY_LIMITWrong Java version selected — check resolution order: BP_JAVA_VERSION → the documented JBP_CONFIG_<NAME>_JRE name (or the auto-generated JBP_CONFIG_<KEY> alias) → manifest default. Enable debug: cf set-env myapp BP_LOG_LEVEL DEBUG.
Adding a standard JRE (one that embeds BaseJRE) requires three things:
- Create
src/java/jres/<name>.go— embedBaseJRE, callnewBaseJRE(), setextraFinalizeOptsif needed - Add dependency to
manifest.yml—name, version, URI, SHA256, stacks; add todefault_versionsif applicable - Register in
src/java/supply/supply.go— onejreRegistry.Register(jres.NewMyJRE(ctx))call
BaseJRE handles everything else: download, extraction, findJavaHome, profile.d script, JVMKill, Memory Calculator, base JVM opts.
For JREs that cannot use BaseJRE (e.g. Zing — no memory calculator, custom detection), implement the full jres.JRE interface manually and use the helper functions listed above.
- DEVELOPING.md — building and running the buildpack locally
- TESTING.md — unit and integration test framework
- IMPLEMENTING_FRAMEWORKS.md — adding framework support
- IMPLEMENTING_CONTAINERS.md — adding container types