Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Java Ubuntu
GUIDEInstall Guides

How to Install Java (OpenJDK 21) on Ubuntu 24.04 VPS

21 min read

How to Install Java (OpenJDK 21) on Ubuntu 24.04 VPS

Java remains one of the most widely deployed runtimes on the server side. From Spring Boot microservices and Tomcat applications to Elasticsearch, Jenkins, and Minecraft servers, a properly configured JDK is the foundation for a huge range of production workloads. This guide walks you through installing OpenJDK 21 LTS on an Ubuntu 24.04 VPS, managing multiple Java versions side by side, configuring JAVA_HOME, deploying applications as systemd services, and tuning the JVM for predictable performance.

Skip the setup? Launch a ready-to-go Linux VPS and have your Java app online in minutes. Deploy a CloudCore Starter VPS and SSH in within 60 seconds.

Table of Contents

  • What is OpenJDK?
  • Why Self-Host Your Java Apps on a VPS?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install OpenJDK 21 from the Default Repository
  • Step 3: Verify the Installation
  • Step 4: Understand JDK vs JRE and Headless Variants
  • Step 5: Install Alternative Distributions (Temurin, Corretto, GraalVM)
  • Step 6: Manage Multiple Java Versions
  • Step 7: Configure JAVA_HOME
  • Step 8: Install Build Tools (Maven, Gradle, SDKMAN)
  • Step 9: Deploy a Jar App as a systemd Service
  • Step 10: Tune the JVM for Production
  • Upgrading Java
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is OpenJDK?

    OpenJDK is the official open-source reference implementation of the Java Platform, Standard Edition. It contains the Java compiler (javac), the HotSpot virtual machine that executes Java bytecode, the standard class libraries, and the JDK tooling (jar, jlink, jshell, jcmd, and others). Nearly every major Java distribution you can download today -- Oracle JDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, GraalVM, Microsoft Build of OpenJDK, Red Hat Build of OpenJDK -- is built from the same OpenJDK source tree. The differences are around the build process, included patches, support terms, and additional tooling, not the underlying Java language or core libraries.

    Java releases follow a six-month cadence, with a Long-Term Support (LTS) release every two years. At the time of writing, the current LTS versions are Java 8, Java 11, Java 17, and Java 21. Java 21, released in September 2023, is the recommended choice for new projects: it introduced virtual threads (Project Loom) for massively concurrent applications, pattern matching for switch, record patterns, and generational ZGC. Java 25 LTS is on the roadmap for September 2025, but Java 21 will remain supported for years.

    Java is deployed across an enormous range of server workloads. Application servers like Apache Tomcat, Jetty, and WildFly host traditional web applications. Modern microservice frameworks like Spring Boot, Quarkus, and Micronaut package applications as runnable jars. Data infrastructure including Elasticsearch, OpenSearch, Kafka, Cassandra, Flink, and Spark are all JVM-based. DevOps tooling such as Jenkins, SonarQube, Artifactory, and Bamboo are Java applications. And on the consumer-server side, everything from Minecraft servers (Paper, Purpur, Forge) to trading algorithm runners depends on a correctly configured JDK.

    Why Self-Host Your Java Apps on a VPS?

    Running your own JVM on a VPS rather than deploying to a managed PaaS offers concrete advantages:

    • Full control over the runtime -- You choose the exact JDK vendor, version, and JVM flags. No managed platform limits you to supported runtimes or locks you out of diagnostic tools like jstack, jcmd, or flight recorder.
    • Predictable, flat-rate cost -- A VPS is billed monthly regardless of CPU minutes or request volume. Heroku-style dynos and Lambda cold starts disappear from your invoice.
    • No cold starts -- The JVM is already warm. Class loading and JIT compilation happen once at boot, so every request hits a tuned, warm runtime.
    • Root access for tuning -- You can set kernel parameters (transparent huge pages, file descriptor limits, swappiness), configure cgroups, and mount large swap files -- none of which is possible on most PaaS offerings.
    • Run anything on the JVM -- Kotlin, Scala, Clojure, Groovy, JRuby, and other JVM languages run on the same OpenJDK install. No per-language buildpack.
    • Long-running workloads -- Batch jobs, stream processors, scheduled jobs, and WebSocket servers run indefinitely without request timeouts or serverless 15-minute caps.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • At least 1 GB of RAM for small Spring Boot apps (2-4 GB recommended for production)
    • At least 5 GB of free disk space for the JDK, your app, and build tooling
    Recommended Plan: CloudCore Starter
    >
    For a single Spring Boot microservice, a Minecraft server for a small group, or a Jenkins controller, the CloudCore Starter plan gives you everything you need:
    >
    - 4 vCPU cores
    - 8 GB RAM
    - 75 GB NVMe SSD
    - Unmetered bandwidth
    >
    For heavier workloads (Elasticsearch clusters, Kafka brokers, or large Spring applications), scale up to a CloudCore Professional or Enterprise plan.

    Connect to your server via SSH:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Refresh the package index and apply pending upgrades. This ensures the Ubuntu repositories advertise the latest OpenJDK builds and that dependency resolution during the install is clean.

    bash
    sudo apt update && sudo apt upgrade -y

    Expected output (abbreviated):

    text
    Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
    Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
    Reading package lists... Done
    Building dependency tree... Done
    0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

    If the kernel was updated, reboot before continuing:

    bash
    sudo reboot

    Step 2: Install OpenJDK 21 from the Default Repository

    Ubuntu 24.04 ships OpenJDK 21 directly in its default noble repositories, which is the simplest and most reliable source for most users. The package is maintained and patched by Ubuntu's security team alongside other system packages.

    Install the full JDK (compiler plus runtime):

    bash
    sudo apt install -y openjdk-21-jdk

    Expected output:

    text
    The following NEW packages will be installed:
      openjdk-21-jdk openjdk-21-jdk-headless openjdk-21-jre
      openjdk-21-jre-headless ca-certificates-java java-common ...
    0 upgraded, 45 newly installed, 0 to remove and 0 not upgraded.
    Need to get 205 MB of archives.
    After this operation, 450 MB of additional disk space will be used.

    Alternatively, install default-jdk, which is a meta-package pointing at the current Ubuntu-preferred JDK (OpenJDK 21 on 24.04):

    bash
    sudo apt install -y default-jdk

    The default-jdk package is convenient for scripts and Dockerfiles that should "just use whatever JDK Ubuntu considers current" -- but if you want a specific version pinned, prefer openjdk-21-jdk directly.

    Step 3: Verify the Installation

    Confirm the Java runtime and compiler are both installed and on the PATH.

    Check the runtime version:

    bash
    java -version

    Expected output:

    text
    openjdk version "21.0.5" 2024-10-15
    OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04)
    OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)

    Check the compiler version:

    bash
    javac -version

    Expected output:

    text
    javac 21.0.5

    Verify the binaries' location:

    bash
    which java javac
    readlink -f $(which java)

    Expected output:

    text
    /usr/bin/java
    /usr/bin/javac
    /usr/lib/jvm/java-21-openjdk-amd64/bin/java

    The actual JDK lives under /usr/lib/jvm/java-21-openjdk-amd64. The /usr/bin/java symlink is managed by Ubuntu's update-alternatives system, which you will use later to switch between multiple installed versions.

    Step 4: Understand JDK vs JRE and Headless Variants

    Ubuntu offers several Java-related packages, and picking the right one keeps your server lean.

    PackageContentsWhen to Use
    openjdk-21-jre-headlessRuntime only, no GUI librariesRunning jar apps on a headless server
    openjdk-21-jreRuntime + AWT/Swing GUI libsRunning GUI Java apps (rare on servers)
    openjdk-21-jdk-headlessJDK + headless JREBuilding and running on a headless server
    openjdk-21-jdkFull JDK including GUIDeveloper workstations
    default-jre / default-jdkMeta-package to current Ubuntu defaultWhen you want "whatever Ubuntu considers current"
    JDK vs JRE:

    • JRE (Java Runtime Environment) contains the JVM and core libraries needed to execute Java bytecode. Install this if you only deploy pre-built .jar or .war files.
    • JDK (Java Development Kit) contains everything in the JRE plus javac (the compiler), jar, jlink, jshell, jstack, jcmd, jmap, and other development and diagnostic tools. Install this if you build from source on the server, or if you want access to production diagnostics like heap dumps and thread stacks.
    Headless vs full:

    The headless variants skip graphical libraries (AWT, Swing, fonts, and X11 dependencies). For a typical server VPS running Spring Boot, Tomcat, Elasticsearch, Jenkins, or any backend process, the headless variant is what you want. It installs roughly 100 MB less, pulls in fewer dependencies, and reduces the attack surface.

    For production deployments, the minimal and most common choice is:

    bash
    sudo apt install -y openjdk-21-jre-headless

    For build servers or when you need diagnostic tooling on the same box:

    bash
    sudo apt install -y openjdk-21-jdk-headless

    Step 5: Install Alternative Distributions (Temurin, Corretto, GraalVM)

    While Ubuntu's OpenJDK is sufficient for most cases, you may need a specific distribution for compatibility, performance, or support reasons.

    Eclipse Temurin (via Adoptium APT repo)

    Eclipse Temurin is the successor to AdoptOpenJDK, distributed by the Eclipse Foundation. It is the most widely adopted community build, TCK-certified, and the default choice for many enterprise stacks. Adoptium provides a dedicated APT repository:

    bash
    sudo apt install -y wget apt-transport-https gnupg
    wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \
      | sudo gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
    echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" \
      | sudo tee /etc/apt/sources.list.d/adoptium.list
    sudo apt update
    sudo apt install -y temurin-21-jdk

    Temurin installs to /usr/lib/jvm/temurin-21-jdk-amd64 and registers itself with update-alternatives automatically.

    Amazon Corretto

    Amazon Corretto is Amazon's no-cost, multiplatform distribution with long-term support. It is the JDK used inside AWS Lambda and ECS Java runtimes, so it is a great match if you are deploying to AWS or want a vendor-backed distribution with security patches.

    bash
    wget -O - https://apt.corretto.aws/corretto.key \
      | sudo gpg --dearmor -o /usr/share/keyrings/corretto.gpg
    echo "deb [signed-by=/usr/share/keyrings/corretto.gpg] https://apt.corretto.aws stable main" \
      | sudo tee /etc/apt/sources.list.d/corretto.list
    sudo apt update
    sudo apt install -y java-21-amazon-corretto-jdk

    GraalVM

    GraalVM is a high-performance JDK with an advanced JIT compiler and a native-image tool that compiles Java applications to standalone native binaries with near-instant startup and small memory footprints. It is the runtime of choice for Quarkus and Micronaut when targeting cloud-native deployment.

    Install GraalVM CE via the official tarball or via SDKMAN (covered later in Step 8):

    bash
    cd /opt
    sudo wget https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz
    sudo tar -xzf graalvm-jdk-21_linux-x64_bin.tar.gz
    sudo rm graalvm-jdk-21_linux-x64_bin.tar.gz
    sudo mv graalvm-jdk-21* graalvm-21
    sudo update-alternatives --install /usr/bin/java java /opt/graalvm-21/bin/java 2100
    sudo update-alternatives --install /usr/bin/javac javac /opt/graalvm-21/bin/javac 2100

    You now have multiple JDKs installed side by side. The next step covers switching between them.

    Step 6: Manage Multiple Java Versions

    Ubuntu's update-alternatives system lets you install multiple JDKs simultaneously and pick which one the java, javac, and related commands resolve to.

    Install another LTS version alongside OpenJDK 21 (for example, Java 17 for a legacy app):

    bash
    sudo apt install -y openjdk-17-jdk-headless

    List all registered Java alternatives:

    bash
    sudo update-alternatives --list java

    Expected output:

    text
    /usr/lib/jvm/java-17-openjdk-amd64/bin/java
    /usr/lib/jvm/java-21-openjdk-amd64/bin/java
    /opt/graalvm-21/bin/java

    Switch interactively between them:

    bash
    sudo update-alternatives --config java

    Expected output:

    text
    There are 3 choices for the alternative java (providing /usr/bin/java).

    Selection Path Priority Status ------------------------------------------------------------

    • 0 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 auto mode
    1 /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1711 manual mode 2 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 manual mode 3 /opt/graalvm-21/bin/java 2100 manual mode

    Press <enter> to keep the current choice[*], or type selection number:

    Repeat the same for javac:

    bash
    sudo update-alternatives --config javac

    This switches the system-wide default. For per-application JDK selection (the recommended approach for production), set JAVA_HOME explicitly in the application's service unit or shell environment -- covered in the next two sections.

    Step 7: Configure JAVA_HOME

    Many Java build tools and frameworks (Maven, Gradle, Tomcat, Elasticsearch) rely on the JAVA_HOME environment variable to locate the JDK rather than using which java.

    System-wide JAVA_HOME

    Find the canonical install directory:

    bash
    readlink -f $(which java) | sed 's|/bin/java||'

    Expected output:

    text
    /usr/lib/jvm/java-21-openjdk-amd64

    Add JAVA_HOME to /etc/environment so it is available to all users and all systemd services:

    bash
    echo 'JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"' | sudo tee -a /etc/environment

    Reload the environment in your current shell:

    bash
    source /etc/environment
    echo $JAVA_HOME

    Expected output:

    text
    /usr/lib/jvm/java-21-openjdk-amd64

    Per-user JAVA_HOME

    If you prefer a user-specific setting, add it to ~/.bashrc or ~/.profile:

    bash
    echo 'export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"' >> ~/.bashrc
    echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.bashrc
    source ~/.bashrc

    Per-app JAVA_HOME (recommended for production)

    Pinning JAVA_HOME inside each app's systemd unit keeps services independent of system-wide changes:

    ini
    [Service]
    Environment="JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64"
    ExecStart=/usr/lib/jvm/temurin-21-jdk-amd64/bin/java -jar /opt/myapp/app.jar

    This pattern lets you upgrade the system default without accidentally restarting services on an incompatible JDK.

    Step 8: Install Build Tools (Maven, Gradle, SDKMAN)

    If you build applications on the server (rather than deploying pre-built jars), install Maven or Gradle.

    Maven from APT

    bash
    sudo apt install -y maven
    mvn -version

    Expected output:

    text
    Apache Maven 3.8.7
    Maven home: /usr/share/maven
    Java version: 21.0.5, vendor: Ubuntu
    Java home: /usr/lib/jvm/java-21-openjdk-amd64

    Gradle via SDKMAN

    Ubuntu's Gradle package is frequently behind the latest release. For up-to-date Gradle, Kotlin, Scala, and SDKMAN-managed JDKs, install SDKMAN:

    bash
    curl -s "https://get.sdkman.io" | bash
    source "$HOME/.sdkman/bin/sdkman-init.sh"

    Install the latest Gradle:

    bash
    sdk install gradle
    gradle -v

    You can also install JDK distributions directly through SDKMAN -- handy for testing without touching system packages:

    bash
    sdk list java
    sdk install java 21.0.5-tem     # Temurin 21
    sdk install java 21.0.5-amzn    # Corretto 21
    sdk install java 21.0.5-graal   # GraalVM 21
    sdk use java 21.0.5-tem         # Switch current shell to Temurin

    SDKMAN-installed JDKs live under ~/.sdkman/candidates/java/ and do not collide with APT-installed packages.

    Step 9: Deploy a Jar App as a systemd Service

    The standard production pattern for a Spring Boot, Quarkus, or Micronaut app is to package it as a fat/über jar and run it as a systemd service. systemd handles automatic restarts, boot-time launch, log integration, and resource limits.

    Prepare the app directory and user

    bash
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
    sudo mkdir -p /opt/myapp
    sudo cp app.jar /opt/myapp/app.jar
    sudo chown -R myapp:myapp /opt/myapp

    Create the systemd unit

    bash
    sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
    [Unit]
    Description=My Spring Boot App
    After=network-online.target
    Wants=network-online.target

    [Service] Type=simple User=myapp Group=myapp WorkingDirectory=/opt/myapp Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64" Environment="SPRING_PROFILES_ACTIVE=prod" Environment="SERVER_PORT=8080" ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java \ -Xms512m -Xmx1g \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/opt/myapp/heapdumps \ -Djava.security.egd=file:/dev/./urandom \ -jar /opt/myapp/app.jar SuccessExitStatus=143 Restart=on-failure RestartSec=10 StandardOutput=journal StandardError=journal LimitNOFILE=65536

    [Install] WantedBy=multi-user.target EOF

    Enable and start the service:

    bash
    sudo mkdir -p /opt/myapp/heapdumps
    sudo chown myapp:myapp /opt/myapp/heapdumps
    sudo systemctl daemon-reload
    sudo systemctl enable --now myapp
    sudo systemctl status myapp

    Tail the app logs:

    bash
    sudo journalctl -u myapp -f

    For a full production-ready systemd walkthrough including security hardening (ProtectSystem, NoNewPrivileges, cgroup limits), see our systemd services guide.

    Expose the app via Nginx

    Front the jar with an Nginx reverse proxy for TLS termination, buffering, and rate limiting -- covered end-to-end in our Nginx reverse proxy tutorial.

    Step 10: Tune the JVM for Production

    The JVM's defaults are conservative. Tuning a handful of flags based on your VPS's RAM and workload pays off quickly.

    Heap sizing: -Xms and -Xmx

    -Xms sets the initial heap, -Xmx sets the maximum. For a server process with a dedicated VPS, set both to the same value so the JVM pre-allocates the heap once and never has to grow (which can cause GC pauses):

    bash
    -Xms1g -Xmx1g

    Rule of thumb for VPS sizing:

    • Reserve at least 512 MB to 1 GB for the OS, metaspace, thread stacks, and native buffers
    • On an 8 GB VPS running a single app: -Xms4g -Xmx4g is a safe starting point
    • On a 2 GB VPS: -Xms1g -Xmx1g
    • On a 16 GB VPS: -Xms10g -Xmx10g
    Alternatively, use -XX:MaxRAMPercentage=75 to let the JVM size the heap as a percentage of container/host RAM -- useful when you run the same jar across different VPS sizes.

    Garbage collectors: G1GC vs ZGC

    G1GC (Garbage First) is the default since Java 9 and the right choice for most workloads on 2-32 GB heaps. It balances throughput and pause times well:

    bash
    -XX:+UseG1GC -XX:MaxGCPauseMillis=200

    ZGC (Z Garbage Collector) is a low-latency collector with sub-millisecond pauses, regardless of heap size. It shines on latency-sensitive services (API gateways, trading, real-time games) and large heaps (16 GB+). Enable generational ZGC in Java 21:

    bash
    -XX:+UseZGC -XX:+ZGenerational

    If you are hitting long GC pauses on G1GC or running with a very large heap, try ZGC. For throughput-heavy batch jobs (ETL, data processing), Parallel GC (-XX:+UseParallelGC) can still be the best choice.

    Useful production flags

    bash
    -XX:+HeapDumpOnOutOfMemoryError            # Dump heap on OOM for post-mortem
    -XX:HeapDumpPath=/opt/myapp/heapdumps      # Where to write the dump
    -XX:+ExitOnOutOfMemoryError                # Let systemd restart on OOM rather than limping
    -XX:+UseStringDeduplication                # Save memory on string-heavy apps (G1GC)
    -Djava.security.egd=file:/dev/./urandom    # Faster SecureRandom seeding
    -XshowSettings:vm                          # Print effective VM settings at startup (debug only)

    Container awareness

    On cgroup-constrained environments (Docker, systemd with MemoryMax=), the JVM respects cgroup limits automatically since Java 10+. No extra flags are needed -- Runtime.availableProcessors() and MaxRAMPercentage both read the cgroup values.

    Upgrading Java

    Security updates (in place)

    Ubuntu's unattended-upgrades covers OpenJDK security updates when configured. Run manual updates with:

    bash
    sudo apt update
    sudo apt upgrade -y openjdk-21-jdk-headless
    sudo systemctl restart myapp

    Major version upgrades (side by side)

    Install the new LTS alongside the current one:

    bash
    sudo apt install -y openjdk-25-jdk-headless   # When Java 25 LTS ships

    Update your app's systemd unit to point at the new JAVA_HOME, reload, and restart:

    bash
    sudo systemctl daemon-reload
    sudo systemctl restart myapp

    Keep the old JDK installed until you have validated the upgrade in production. Roll back by flipping the JAVA_HOME and ExecStart paths in the unit file.

    Troubleshooting

    ProblemCauseSolution
    java: command not foundPackage not installed or PATH not refreshedRun sudo apt install -y openjdk-21-jre-headless, then hash -r or reopen the shell
    JAVA_HOME is not defined correctlyEnvironment variable set to a JRE or missingCheck echo $JAVA_HOME; confirm with $JAVA_HOME/bin/javac -version; update /etc/environment
    OutOfMemoryError: Java heap spaceHeap exhausted under loadIncrease -Xmx, investigate heap dump (-XX:+HeapDumpOnOutOfMemoryError), look for leaks with VisualVM or Eclipse MAT
    OutOfMemoryError: MetaspaceToo many loaded classes (common with hot-reload)Raise -XX:MaxMetaspaceSize=512m; restart service; check for classloader leaks
    Long GC pauses (>1s)G1GC struggling with heap size or allocation rateTry -XX:+UseZGC -XX:+ZGenerational; lower -XX:MaxGCPauseMillis; profile with jcmd <pid> GC.heap_info
    Wrong JDK picked up by MavenJAVA_HOME differs from update-alternatives defaultSet JAVA_HOME explicitly in ~/.bashrc or /etc/environment; verify with mvn -version
    Unsupported class file major version 65Building with newer JDK than runtimeMatch build JDK to runtime, or set <maven.compiler.release>21</maven.compiler.release>
    App killed by OOM killer (dmesg)Host RAM exhausted, JVM heap uncappedSet -Xmx explicitly; add swap; monitor with free -h and journalctl -k
    Slow startup on small VPSClass loading and JIT warm-upUse -XX:+UseAppCDS class data sharing or switch to GraalVM native-image for instant startup

    Useful diagnostic commands

    bash
    # What's running on the JVM?
    jps -lv

    Live thread dump

    jcmd <pid> Thread.print

    Heap summary

    jcmd <pid> GC.heap_info

    Live heap dump

    jcmd <pid> GC.heap_dump /tmp/heap.hprof

    VM flags in effect

    jcmd <pid> VM.flags

    These tools ship with the JDK (not the JRE), which is one reason to install the headless JDK even on production hosts.

    FAQ

    Should I install OpenJDK, Oracle JDK, Temurin, or Corretto?

    For most Ubuntu VPS deployments, the default Ubuntu OpenJDK package is the right choice: it is patched alongside the rest of the system and requires no extra repositories. Choose Eclipse Temurin if you want the most widely adopted community build with quarterly updates independent of Ubuntu's release cadence. Choose Amazon Corretto if you are deploying to AWS or want binary parity with Lambda/ECS. Choose Oracle JDK only if your organization has a commercial Oracle subscription -- Oracle's standard terms require payment for commercial use of the Oracle JDK on servers, while OpenJDK, Temurin, and Corretto are free for any use.

    What is the difference between Java 8, 11, 17, and 21?

    All four are LTS releases. Java 8 is still widely deployed in legacy systems but is approaching end of life for free public updates; avoid it for new projects. Java 11 added modules, a new HTTP client, and var. Java 17 added sealed classes, pattern matching, and records. Java 21 introduced virtual threads (Project Loom), pattern matching for switch, record patterns, and generational ZGC. For new projects, start on 21. For existing apps, upgrade from 8 to 17 or 21 as soon as your dependencies allow -- the performance and security improvements are substantial.

    How much RAM does the JVM actually use beyond -Xmx?

    The -Xmx flag caps only the Java heap. A running JVM also uses memory for metaspace (class metadata, typically 100-300 MB), thread stacks (1 MB default per thread), code cache (JIT-compiled native code, ~240 MB), direct byte buffers (NIO off-heap), and native library allocations. A rough estimate for total resident memory is Xmx + 512 MB. On a 1 GB VPS running a 512 MB heap, expect the JVM process to use around 900-1100 MB of RSS under load. Always leave headroom to avoid OOM kills.

    Can I run Java 21 apps built on Java 17?

    Yes -- Java maintains strong backwards compatibility. Bytecode built with --release 17 runs unchanged on Java 21. The reverse is not true: bytecode compiled targeting Java 21 will not run on a Java 17 runtime and throws UnsupportedClassVersionError. When building jars, pin the target explicitly with <maven.compiler.release>17</maven.compiler.release> (Maven) or sourceCompatibility = 17 (Gradle) to keep compatibility predictable.

    Do I need a JDK or just a JRE to run my app?

    A JRE is sufficient to execute a pre-built jar. However, we recommend installing the headless JDK on production servers anyway because it includes diagnostic tools (jcmd, jstack, jmap, jfr) that are invaluable when troubleshooting live incidents. The size and attack-surface difference between openjdk-21-jre-headless and openjdk-21-jdk-headless is small, and you will thank yourself the first time production needs a thread dump.

    Next Steps

    Now that Java is installed and tuned, here are recommended next steps:

    • Front your app with Nginx and TLS -- Add a reverse proxy for HTTPS, gzip, and rate limiting. See our Nginx reverse proxy on Ubuntu guide.
    • Harden your systemd unit -- Apply sandboxing flags like ProtectSystem=strict, PrivateTmp=true, and NoNewPrivileges=true. Full walkthrough in our systemd services guide.
    • Monitor the JVM -- Expose Micrometer or Prometheus JMX metrics from your app and scrape them into Grafana for heap, GC, and thread-pool visibility.
    • Try GraalVM native-image -- For Quarkus or Spring Boot 3 apps, compile to a native binary with sub-100 ms startup and half the RAM footprint. Great for small VPS tiers.
    • Explore SDKMAN managed JDKs -- Visit sdkman.io to swap between vendors and versions per shell without reinstalling packages.
    • Read the upstream docs -- The OpenJDK project and Adoptium Temurin sites publish release notes, security advisories, and tuning guides for each release.

    Skip the Manual Install -- Launch a CloudCore VPS
    >
    Our CloudCore Starter plans give you everything you need to run Java apps in production: 4 vCPU, 8 GB RAM, 75 GB NVMe SSD, Ubuntu 24.04 pre-installed, and unmetered bandwidth. Deploy in 60 seconds and have your Spring Boot app online the same day.
    >
    - Root SSH access and full JVM control
    - Swap any JDK vendor (OpenJDK, Temurin, Corretto, GraalVM) you like
    - systemd, Nginx, and TLS all supported out of the box
    - 24/7 infrastructure support
    >
    Deploy Your CloudCore Starter VPS -- Get started today.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket